diff --git a/.gitignore b/.gitignore index ae6d411..75018f4 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,6 @@ WikiRaces/report.xml WikiRaces/Preview.html WikiRaces/screenshots WikiRaces/metadata -WikiRaces/test_output WikiRaces/fabric.apikey WikiRaces/fabric.buildsecret @@ -73,3 +72,15 @@ WikiRaces/fabric.buildsecret WikiRaces/WikiRaces/GoogleService-Info.plist WKRPython/env + +WKRPython/geckodriver.log + +WikiRaces/fabric.buildsecret + +WikiRaces/fabric.apikey + +WikiRaces/culprits.txt + +WikiRaces/fastlane/report.xml + +WikiRaces/fastlane/README.md diff --git a/.travis.yml b/.travis.yml index a941e7e..73f49ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: objective-c -osx_image: xcode10 +osx_image: xcode10.2 matrix: include: - env: SCHEME="WKRKit" TYPE="build" + - env: SCHEME="WKRKitOfflineTests" TYPE="test" - env: SCHEME="WKRUIKit" TYPE="build" - - env: SCHEME="WikiRaces-MW" TYPE="build" + - env: SCHEME="WikiRacesTests" TYPE="test" + - env: SCHEME="WikiRaces" TYPE="build" before_install: - chmod +x install_swiftlint.sh @@ -13,4 +15,4 @@ install: - ./install_swiftlint.sh script: - - "xcodebuild clean $TYPE -workspace WikiRaces.xcworkspace -scheme $SCHEME -destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch),OS=11.2'" + - "xcodebuild clean $TYPE -workspace WikiRaces.xcworkspace -scheme $SCHEME -destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch),OS=12.2'" diff --git a/README.md b/README.md index c553fa0..90646f9 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ Feel free to ping me with questions. ### Root Directory #### /Resources -- Various iconography. -#### /WKR Python +- Various design resources. +#### /WKRPython - Python scripts I use to compile the final articles list. +#### /WKRArticlesPreviewer +- macOS app for previewing the final articles list. #### /WKRCloudStats - macOS app that pulls app analytics from CloudKit. #### /WKRKit diff --git a/Resources/Design.key b/Resources/Design.key new file mode 100755 index 0000000..c0162d8 Binary files /dev/null and b/Resources/Design.key differ diff --git a/Resources/WKRSketch.sketch b/Resources/WKRSketch.sketch index 76f3610..f1a9561 100644 Binary files a/Resources/WKRSketch.sketch and b/Resources/WKRSketch.sketch differ diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer.xcodeproj/project.pbxproj b/WKRArticlesPreviewer/WKRArticlesPreviewer.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9c3bd40 --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer.xcodeproj/project.pbxproj @@ -0,0 +1,337 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1434C25222238E5C0099C53A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434C25122238E5C0099C53A /* AppDelegate.swift */; }; + 1434C25422238E5C0099C53A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434C25322238E5C0099C53A /* ViewController.swift */; }; + 1434C25622238E5D0099C53A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1434C25522238E5D0099C53A /* Assets.xcassets */; }; + 1434C25922238E5D0099C53A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1434C25722238E5D0099C53A /* Main.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1434C24E22238E5C0099C53A /* WKRArticlesPreviewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WKRArticlesPreviewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1434C25122238E5C0099C53A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1434C25322238E5C0099C53A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 1434C25522238E5D0099C53A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1434C25822238E5D0099C53A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1434C25A22238E5D0099C53A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1434C25B22238E5D0099C53A /* WKRArticlesPreviewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WKRArticlesPreviewer.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1434C24B22238E5C0099C53A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1434C24522238E5C0099C53A = { + isa = PBXGroup; + children = ( + 1434C25022238E5C0099C53A /* WKRArticlesPreviewer */, + 1434C24F22238E5C0099C53A /* Products */, + ); + sourceTree = ""; + }; + 1434C24F22238E5C0099C53A /* Products */ = { + isa = PBXGroup; + children = ( + 1434C24E22238E5C0099C53A /* WKRArticlesPreviewer.app */, + ); + name = Products; + sourceTree = ""; + }; + 1434C25022238E5C0099C53A /* WKRArticlesPreviewer */ = { + isa = PBXGroup; + children = ( + 1434C25122238E5C0099C53A /* AppDelegate.swift */, + 1434C25322238E5C0099C53A /* ViewController.swift */, + 1434C25522238E5D0099C53A /* Assets.xcassets */, + 1434C25722238E5D0099C53A /* Main.storyboard */, + 1434C25A22238E5D0099C53A /* Info.plist */, + 1434C25B22238E5D0099C53A /* WKRArticlesPreviewer.entitlements */, + ); + path = WKRArticlesPreviewer; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1434C24D22238E5C0099C53A /* WKRArticlesPreviewer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1434C25E22238E5D0099C53A /* Build configuration list for PBXNativeTarget "WKRArticlesPreviewer" */; + buildPhases = ( + 1434C24A22238E5C0099C53A /* Sources */, + 1434C24B22238E5C0099C53A /* Frameworks */, + 1434C24C22238E5C0099C53A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WKRArticlesPreviewer; + productName = WKRArticlesPreviewer; + productReference = 1434C24E22238E5C0099C53A /* WKRArticlesPreviewer.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1434C24622238E5C0099C53A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1020; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "Andrew Finke"; + TargetAttributes = { + 1434C24D22238E5C0099C53A = { + CreatedOnToolsVersion = 10.2; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + }; + }; + buildConfigurationList = 1434C24922238E5C0099C53A /* Build configuration list for PBXProject "WKRArticlesPreviewer" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1434C24522238E5C0099C53A; + productRefGroup = 1434C24F22238E5C0099C53A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1434C24D22238E5C0099C53A /* WKRArticlesPreviewer */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1434C24C22238E5C0099C53A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1434C25622238E5D0099C53A /* Assets.xcassets in Resources */, + 1434C25922238E5D0099C53A /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1434C24A22238E5C0099C53A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1434C25422238E5C0099C53A /* ViewController.swift in Sources */, + 1434C25222238E5C0099C53A /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 1434C25722238E5D0099C53A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1434C25822238E5D0099C53A /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1434C25C22238E5D0099C53A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1434C25D22238E5D0099C53A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 1434C25F22238E5D0099C53A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = WKRArticlesPreviewer/WKRArticlesPreviewer.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 72S993BNAV; + INFOPLIST_FILE = WKRArticlesPreviewer/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRArticlesPreviewer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 1434C26022238E5D0099C53A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = WKRArticlesPreviewer/WKRArticlesPreviewer.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 72S993BNAV; + INFOPLIST_FILE = WKRArticlesPreviewer/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRArticlesPreviewer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1434C24922238E5C0099C53A /* Build configuration list for PBXProject "WKRArticlesPreviewer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1434C25C22238E5D0099C53A /* Debug */, + 1434C25D22238E5D0099C53A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1434C25E22238E5D0099C53A /* Build configuration list for PBXNativeTarget "WKRArticlesPreviewer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1434C25F22238E5D0099C53A /* Debug */, + 1434C26022238E5D0099C53A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 1434C24622238E5C0099C53A /* Project object */; +} diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer/AppDelegate.swift b/WKRArticlesPreviewer/WKRArticlesPreviewer/AppDelegate.swift new file mode 100644 index 0000000..5e89cee --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer/AppDelegate.swift @@ -0,0 +1,16 @@ +// +// AppDelegate.swift +// WKRArticlesPreviewer +// +// Created by Andrew Finke on 2/24/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer/Assets.xcassets/AppIcon.appiconset/Contents.json b/WKRArticlesPreviewer/WKRArticlesPreviewer/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2db2b1c --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer/Assets.xcassets/Contents.json b/WKRArticlesPreviewer/WKRArticlesPreviewer/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer/Base.lproj/Main.storyboard b/WKRArticlesPreviewer/WKRArticlesPreviewer/Base.lproj/Main.storyboard new file mode 100644 index 0000000..fd2521d --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer/Base.lproj/Main.storyboard @@ -0,0 +1,805 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer/Info.plist b/WKRArticlesPreviewer/WKRArticlesPreviewer/Info.plist new file mode 100644 index 0000000..bfff29f --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer/Info.plist @@ -0,0 +1,37 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2019 Andrew Finke. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer/ViewController.swift b/WKRArticlesPreviewer/WKRArticlesPreviewer/ViewController.swift new file mode 100644 index 0000000..e095358 --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer/ViewController.swift @@ -0,0 +1,94 @@ +// +// ViewController.swift +// WKRArticlesPreviewer +// +// Created by Andrew Finke on 2/24/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import Cocoa +import WebKit + +class ViewController: NSViewController { + + // MARK: - Properties + + @IBOutlet weak var webView: WKWebView! + var selectButton: NSButton? + + var remainingArticles = [String]() + var keepArticles = [String]() + var removeArticles = [String]() + + var lastArticle = "" + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + webView.load(URLRequest(url: URL(string: "https://en.m.wikipedia.org")!)) + NSWorkspace.shared.openFile(NSTemporaryDirectory()) + } + + func moveToNextArticle(keepCurrent: Bool) { + let article = remainingArticles.removeFirst() + lastArticle = article + if keepCurrent { + keepArticles.append(article) + } else { + removeArticles.append(article) + } + save(keepArticles, named: "keep") + save(removeArticles, named: "remove") + showNextArticle() + } + + func save(_ array: [String], named name: String) { + let path = NSTemporaryDirectory() + name + ".plist" + NSMutableArray(array: array).write(toFile: path, atomically: false) + } + + func showNextArticle() { + let article = remainingArticles.first ?? "" + guard let url = URL(string: "https://en.m.wikipedia.org/wiki" + article) else { fatalError() } + webView.load(URLRequest(url: url)) + selectButton?.title = remainingArticles.count.description + } + + // MARK: - Actions + + @IBAction func selectArticlesList(_ sender: NSButton) { + let dialog = NSOpenPanel() + dialog.title = "Choose articles file" + dialog.allowsMultipleSelection = false + dialog.allowedFileTypes = ["plist"] + + if dialog.runModal() == .OK, let result = dialog.url { + guard let plist = NSArray(contentsOf: result) as? [String] else { + fatalError() + } + remainingArticles = plist.sorted() + showNextArticle() + } + selectButton = sender + } + + @IBAction func keepArticle(_ sender: Any) { + moveToNextArticle(keepCurrent: true) + } + + @IBAction func removeArticle(_ sender: Any) { + moveToNextArticle(keepCurrent: false) + } + + @IBAction func undoLastAction(_ sender: Any) { + if keepArticles.last == lastArticle { + keepArticles.removeLast() + } else if removeArticles.last == lastArticle { + removeArticles.removeLast() + } + remainingArticles.insert(lastArticle, at: 0) + showNextArticle() + } + +} diff --git a/WKRArticlesPreviewer/WKRArticlesPreviewer/WKRArticlesPreviewer.entitlements b/WKRArticlesPreviewer/WKRArticlesPreviewer/WKRArticlesPreviewer.entitlements new file mode 100644 index 0000000..40b639e --- /dev/null +++ b/WKRArticlesPreviewer/WKRArticlesPreviewer/WKRArticlesPreviewer.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/WKRCloudStats/WKRCloudStats.xcodeproj/project.pbxproj b/WKRCloudStats/WKRCloudStats.xcodeproj/project.pbxproj index ca89528..7a43a7f 100644 --- a/WKRCloudStats/WKRCloudStats.xcodeproj/project.pbxproj +++ b/WKRCloudStats/WKRCloudStats.xcodeproj/project.pbxproj @@ -113,6 +113,7 @@ TargetAttributes = { 145623D31F8DE63C00B1ECAC = { CreatedOnToolsVersion = 9.0; + LastSwiftMigration = 1020; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Push = { @@ -306,7 +307,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRCloudStats; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -322,7 +323,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WKRCloudStats; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme b/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme index b6c96a9..9b1badd 100644 --- a/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme +++ b/WKRCloudStats/WKRCloudStats.xcodeproj/xcshareddata/xcschemes/WKRCloudStats.xcscheme @@ -1,6 +1,6 @@ + + + + classNames + + WKRKitPageFetcherTests + + testConnectionTester() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.19795 + baselineIntegrationDisplayName + Mar 1, 2019 at 1:05:47 AM + + + testError() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00041919 + baselineIntegrationDisplayName + Mar 1, 2019 at 1:40:20 AM + + + testLinkedPageFetcher() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 4.0901 + baselineIntegrationDisplayName + Mar 1, 2019 at 1:05:47 AM + + + testPage() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.12706 + baselineIntegrationDisplayName + Mar 1, 2019 at 1:40:20 AM + + + testRandom() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.456 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 200 + + + testSource() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.18445 + baselineIntegrationDisplayName + Mar 1, 2019 at 1:40:20 AM + + + testURL() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.1892 + baselineIntegrationDisplayName + Mar 1, 2019 at 1:40:20 AM + + + + + + diff --git a/WKRKit/WKRKit.xcodeproj/xcshareddata/xcbaselines/149FF8101F362B3D000A5D96.xcbaseline/Info.plist b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcbaselines/149FF8101F362B3D000A5D96.xcbaseline/Info.plist new file mode 100644 index 0000000..44f7e5c --- /dev/null +++ b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcbaselines/149FF8101F362B3D000A5D96.xcbaseline/Info.plist @@ -0,0 +1,40 @@ + + + + + runDestinationsByUUID + + 25423EE2-9723-4620-97A9-4F3486F1276C + + localComputer + + busSpeedInMHz + 100 + cpuCount + 1 + cpuKind + Intel Core i7 + cpuSpeedInMHz + 3100 + logicalCPUCoresPerPackage + 8 + modelCode + MacBookPro14,3 + physicalCPUCoresPerPackage + 4 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + targetDevice + + modelCode + iPhone11,2 + platformIdentifier + com.apple.platform.iphonesimulator + + + + + diff --git a/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKit.xcscheme b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKit.xcscheme index 08e6cf7..cd7f5dc 100644 --- a/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKit.xcscheme +++ b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKit.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitTests.xcscheme b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitTests.xcscheme index 7a63329..eb553a4 100644 --- a/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitTests.xcscheme +++ b/WKRKit/WKRKit.xcodeproj/xcshareddata/xcschemes/WKRKitTests.xcscheme @@ -1,6 +1,6 @@ + + + + + skipped = "NO" + parallelizable = "YES" + testExecutionOrdering = "random"> + /10_Downing_Street + /15th_century + /16-bit + /16th_century + /18th_century + /19th_century + /20_July_plot + /20th_century /20th_Century_Fox + /20th_Television + /21st_century /21st_Century_Fox /23andMe /2D_computer_graphics + /2nd_millennium_BC + /32-bit /3D_computer_graphics - /3D_Entertainment /3D_film /3D_modeling /3D_printing @@ -15,14 +26,23 @@ /3D_rendering /3D_scanner /3G + /4-8-4 /4chan /4D_film /4G /4K_resolution + /60_Minutes + /64-bit_computing /7-Eleven + /70_mm_film + /720p + /8-bit /8-track_tape /8_mm_video_format /A_Bug%27s_Life + /A_cappella + /A_Goofy_Movie + /A_Pup_Named_Scooby-Doo /A_Song_of_Ice_and_Fire /Aardvark /Aardwolf @@ -44,6 +64,7 @@ /Abraham_Lincoln /Abrasive /Absinthe + /Absolut_Vodka /Abstract_algebra /Abstract_art /Abstract_expressionism @@ -52,10 +73,12 @@ /Absurdism /Absurdist_fiction /Absurdity + /Abu_Dhabi /AC/DC /Academi /Academic_art /Academic_conference + /Academic_degree /Academic_dishonesty /Academic_freedom /Academic_journal @@ -66,9 +89,7 @@ /Academy_of_sciences /Accattone /Accelerating_change - /Accentor /Accenture - /Accession_day /Accompaniment /Accountant /Accounting @@ -80,12 +101,16 @@ /Acid /Acid_rain /Acid_rock + /Acne /Acosmism /Acoustic_guitar + /Acoustic_music /Acoustical_engineering /Acoustics /Acquire /Acrobatics + /Acronym + /Acropolis /Acropolis_of_Athens /Acting /Acting_white @@ -97,6 +122,7 @@ /Action_game /Action_Man /Action_potential + /Action_role-playing_game /Activation_energy /Active_Directory /Activision @@ -110,6 +136,7 @@ /Adam_Driver /Adam_Sandler /Adaptation + /Adaptive_reuse /Addiction /Addition /Adele @@ -118,17 +145,20 @@ /Adidas /Adirondack_Park /Adjacency_list + /Adjacency_matrix /Adjective /Administrative_law /Admiralty_law + /Adobe_Acrobat /Adobe_Flash + /Adobe_Illustrator /Adobe_Photoshop - /Adobe_Systems + /Adobe_Premiere_Pro /Adolescence /Adolf_Hitler /Adrenaline + /Adsorption /Adult - /Adult_learner /Advance_Publications /Advanced_capitalism /Advanced_Micro_Devices @@ -143,6 +173,7 @@ /Advocacy /Advocacy_group /Advocate + /ADX_Florence /Aegithalidae /Aerial_photography /Aerial_tramway @@ -172,6 +203,9 @@ /African_Americans /African_art /African_buffalo + /African_bush_elephant + /African_cuisine + /African_elephant /African_philosophy /African_spoonbill /African_studies @@ -181,11 +215,15 @@ /Africanized_bee /Afro /Afterlife + /Agar + /Age_of_Discovery /Age_of_Empires /Age_of_Empires_II /Age_of_Empires_III + /Age_of_Enlightenment /Age_of_Ultron /Ageing + /Agent_Orange /Agents_of_S.H.I.E.L.D. /Aggregate_demand /Aggression @@ -204,7 +242,9 @@ /Aida /Aim_for_the_Ace! /Air_Canada + /Air_charter /Air_China + /Air_combat_manoeuvring /Air_conditioning /Air_France /Air_hockey @@ -214,6 +254,7 @@ /Air_New_Zealand /Air_pollution /Air_show + /Air_taxi /Air_traffic_control /Air_travel /AirAsia @@ -227,7 +268,10 @@ /Aircraft /Aircraft_engine /Aircraft_pilot + /Aircraft_safety_card /Airline + /Airline_alliance + /Airline_timetable /Airliner /Airmail /Airplane @@ -247,8 +291,10 @@ /Alabama_Sports_Festival /Aladdin /Alamo_Drafthouse_Cinema + /Alan_Tudyk /Alan_Turing /Alaska + /Alaska_Airlines /Alaska_Purchase /Albania /Albany_Congress @@ -258,10 +304,12 @@ /Alberta /Albertville /Album + /Album-equivalent_unit /Albus_Dumbledore /Alcatraz_Island /Alchemy /Alcohol + /Alcohol_intoxication /Alcoholic_drink /Alcoholism /Aldford @@ -275,6 +323,7 @@ /Alexandria /Alfred_Hitchcock /Algae + /Algal_bloom /Algebra /Algebraic_curve /Algebraic_geometry @@ -287,6 +336,8 @@ /Algorithm /Alice_Faye /Alice_in_Chains + /Alien_and_Sedition_Acts + /Alkali /Alkali_metal /Alkaline_earth_metal /All-terrain_vehicle @@ -300,6 +351,8 @@ /Allophilia /Allotropy /Alloy + /Alma_mater + /Almond /Almond_milk /Alpaca /Alpha_Centauri @@ -313,6 +366,7 @@ /Alps /Alquerque /Alsace_wine + /Alt-right /Altai_Mountains /AltaVista /Alter_ego @@ -320,26 +374,38 @@ /Altered_Beast /Alternate_history /Alternating_current + /Alternative_energy + /Alternative_metal /Alternative_rock - /Altricial /Altruism /Aluminium /Aluminium_alloy + /Aluminum_Hall_of_Fame /Amateur /Amateur_radio + /Amazon_Appstore + /Amazon_Echo /Amazon_Kindle + /Amazon_Prime /Amazon_rainforest /Amazon_River /Amazon_Video /Amazon_Web_Services + /Ambient_music /AMC_Networks /Amchitka + /America_on_Parade + /America_Sings /American_Airlines + /American_alligator /American_bison + /American_bullfrog /American_Civil_War /American_comic_book /American_Crime_Story /American_crocodile + /American_cuisine + /American_Dad! /American_Dream /American_English /American_exceptionalism @@ -349,11 +415,13 @@ /American_frontier /American_Graffiti /American_Hustle + /American_Idiot /American_Idol /American_imperialism /American_Indian_Wars /American_League /American_middle_class + /American_Ninja_Warrior /American_philosophy /American_Red_Cross /American_Revolution @@ -362,10 +430,10 @@ /American_wine /Americas /AmigaOS + /Amiibo /Amino_acid /Amish /Ammonia - /Ammonia_solution /Ammunition /Amor_asteroid /Amorality @@ -375,6 +443,7 @@ /Amphibian /Amphibious_vehicle /Amphiprioninae + /Amphitheatre /Amplifier /Amputee_football /Amsterdam @@ -384,16 +453,18 @@ /Amusement_park /Amy_Adams /Amy_Poehler - /Amy_Rose /Amygdala /Amygdalin /Anaerobic_exercise /Analgesic /Analog_computer /Analog_Devices + /Analog_signal + /Analog_synthesizer /Analog_television /Analogue_electronics /Analogy + /Analysis /Analytic_geometry /Analytical_chemistry /Analytical_Engine @@ -428,8 +499,8 @@ /Andromeda_Galaxy /Andy_Hertzfeld /Andy_Warhol - /Anesthesiologist /Angel_Beats! + /Angel_Stadium /Angelina_Jolie /Anger /Angkor_Wat @@ -438,8 +509,11 @@ /Anglicanism /Anglosphere /Angry_Birds + /Angus_cattle + /Animagique /Animal /Animal_coloration + /Animal_communication /Animal_Crossing /Animal_echolocation /Animal_Farm @@ -452,10 +526,13 @@ /Animal_training /Animal_welfare /Animaniacs + /Animated_cartoon + /Animated_documentary /Animated_series /Animated_sitcom /Animation /Animation_Academy + /Animation_studio /Animator /Anime /Animism @@ -482,12 +559,15 @@ /Antbird /Anteater /Antelope + /Anthem /Anthocyanin + /Anthology_series /Anthrax /Anthropologist /Anthropology /Anthropomorphism /Anthrozoology + /Anti-gravity /Antibiotic /Antibody /Anticyclone @@ -496,6 +576,7 @@ /Antimicrobial /Antimony /Antipositivism + /Antique /Antireligion /Antisemitism /Antitheism @@ -536,7 +617,6 @@ /Apple_butter /Apple_cake /Apple_Campus - /Apple_chips /Apple_cider /Apple_community /Apple_Corps @@ -545,6 +625,7 @@ /Apple_I /Apple_ID /Apple_II + /Apple_II_Plus /Apple_II_series /Apple_IIe /Apple_IIe_Card @@ -578,11 +659,12 @@ /Applied_mechanics /Applied_physics /Applied_psychology - /Applied_research /Applied_science /Appreciation_Index /Appropriate_technology + /April_Fools%27_Day /Apsis + /Aquaman /Aquarium /Aqueous_solution /Aquifer @@ -592,6 +674,7 @@ /Arabian_Peninsula /Arabic /Arabic_alphabet + /Arabic_grammar /Arabic_literature /Arabic_poetry /Arabic_script @@ -599,6 +682,7 @@ /Arachnology /Aragon /Aramaic_language + /Arby%27s /Arc_lamp /Arcade_cabinet /Arcade_game @@ -606,15 +690,18 @@ /Arch_bridge /Archaea /Archaeoastronomy + /Archaeological_site /Archaeology /Archaeopteryx /Archenemy /Archery + /Arches_National_Park /Archetype /Archimedean_solid /Archimedes /Archipelago /Architect + /Architectural_firm /Architectural_historian /Architectural_plan /Architectural_style @@ -626,11 +713,14 @@ /Arctic_Monkeys /Arctic_Ocean /Arctic_Winter_Games + /Arduino /Area /Area_51 /Area_studies /Areas_of_mathematics + /Arena /Arena_football + /Arena_rock /Ares /Argentina /Argentine_austral @@ -640,6 +730,7 @@ /Argument /Argument_from_authority /Argumentation_theory + /Ariel%27s_Grotto /Aristocracy /Aristophanes /Aristotelianism @@ -647,6 +738,7 @@ /Arithmetic /Arizona /Arizona_Territory + /Ark_of_the_Covenant /Arkansas /Armadillo /Armenia @@ -657,8 +749,11 @@ /Arms_embargo /Arms_industry /Arms_race + /Army_men /Arne_Jacobsen + /Arno /Arnold_Schwarzenegger + /ARPANET /Arrhenius_equation /Ars_Technica /Arsenal_F.C. @@ -666,19 +761,27 @@ /Arson /Art /Art_colony + /Art_critic + /Art_criticism /Art_Deco /Art_director /Art_Directors_Guild /Art_film + /Art_game + /Art_glass /Art_history + /Art_Institute_of_Chicago /Art_intervention /Art_Linkletter /Art_movement /Art_museum /Art_music /Art_Nouveau + /Art_of_Disney_Animation /Art_rock + /Art_therapy /Artamidae + /Artemis /Arthropod /Arthur_Compton /Artificial_heart @@ -691,6 +794,7 @@ /Arts_centre /Asbestos /ASCII + /Ascot_tie /Asia /Asia-Pacific /Asian_Americans @@ -705,9 +809,11 @@ /Asian_Winter_Games /Asian_Youth_Games /Ask.com + /Asparagus /Asperger_syndrome /Aspergirls /Asphalt + /Asphyxia /Aspirin /Assam /Assamese_language @@ -744,6 +850,7 @@ /Astronomy /Astrophysics /Asus + /AT%26T /At_sign /Atalanta /Atari @@ -785,6 +892,8 @@ /Attention /Attila /Attitude_change + /Aubrey_Plaza + /Auckland /Auction /Audi /Audi_A4 @@ -794,7 +903,7 @@ /Audio_engineer /Audio_equipment /Audio_game - /Audio_tour + /Audio_signal /Audiobook /Audioslave /Audition @@ -828,17 +937,21 @@ /AutoCAD /Autodesk /Autodidacticism + /Autograph /Automata_theory /Automatic_parking /Automation + /Automotive_battery /Automotive_engineering /Automotive_industry /Autonomous_car /Autonomy /Autopia + /Autopsy /Autostereogram /Autostereoscopy /Autotroph + /Avatar_Flight_of_Passage /Avaya /Aviation /Aviation_safety @@ -847,6 +960,7 @@ /Avionics /Avocado /Avogadro_constant + /Avon_Products /Awareness /Awe /Axe @@ -859,8 +973,8 @@ /Axle /Ayrshire /Azerbaijan - /Aztec /Aztec_Empire + /Aztecs /Babe_Ruth /Baboon /Baby_boomers @@ -868,6 +982,10 @@ /Baby_transport /Babylon /Babylonia + /Babylonian_mathematics + /Bachelor + /Bachelor_of_Arts + /Bachelor_of_Commerce /Back_to_the_Future /Backgammon /Backlot @@ -883,11 +1001,15 @@ /Bacteria /Bactrian_camel /Bad_Aibling + /Bad_breath /Bad_Robot_Productions + /Badge /Badger /Badlands + /Badlands_National_Park /Badminton /BAFTA_Fellowship + /Bagel /Baghdad /Bagpipes /Bahrain @@ -898,6 +1020,7 @@ /Bakery /Baking /Balance_beam + /Balance_of_nature /Bald_eagle /Balkans /Ball @@ -907,9 +1030,11 @@ /Ballet /Ballista /Ballistic_missile + /Ballistics /Ballroom /Ballroom_dance /Bally_Technologies + /Balmoral_Castle /Balrog /Baltic_Sea /Baltic_Shield @@ -917,6 +1042,10 @@ /Bambi /Bamboo /Banach_algebra + /Banana + /Banana_bread + /Banana_ketchup + /Banana_leaf /Bananaquit /Band_Hero /Band_society @@ -938,28 +1067,37 @@ /Barbara_McClintock /Barbarian /Barbecue + /Barbecue_sauce /Barber /Barbie /Barcelona /Barclays /Barcode + /Barefoot /Barge /Bariatric_surgery + /Barley /Barn-owl /Barnacle + /Barnes_%26_Noble + /Barney_%26_Friends + /Barnstorming /Baroclinity /Barometer /Baroque /Barracks /Barracuda + /Barriers_to_entry /Barry_Bonds /Barry_Goldwater /Bart_Simpson /Bartender + /Barter /Baruch_Spinoza /Bascule_bridge /Baseball /Baseball_bat + /Baseball_card /Baseball_field /Baseball_park /Basement @@ -968,10 +1106,12 @@ /Basilisk /Basis_point /Basketball + /Baskin-Robbins /Basque_pelota /Basra /Bass_guitar /Bassaricyon + /Basset_Hound /Bassist /Bassoon /Bat @@ -993,9 +1133,20 @@ /Battle /Battle_axe /Battle_of_Britain + /Battle_of_Champion_Hill + /Battle_of_Chickamauga + /Battle_of_Cold_Harbor + /Battle_of_Fort_Donelson /Battle_of_France + /Battle_of_Fredericksburg + /Battle_of_Gettysburg + /Battle_of_Malvern_Hill + /Battle_of_Midway + /Battle_of_the_Crater + /Battle_of_York /Battle_royale_game /BattleBots + /Battlecruiser /Battlefield_2 /Battleship /Batwoman @@ -1003,6 +1154,7 @@ /Bavaria /Bay /Bay_Lake_Tower + /Bay_leaf /Bayesian_network /Bayeux_Tapestry /Baylor_University @@ -1014,10 +1166,12 @@ /BBC_News /BBC_One /BBC_Online + /BBC_Parliament /BBC_Radio /BBC_Sport /BBC_Television /BBC_Two + /BBC_World_Service /BC_Rail /Be_Our_Guest /Be_Our_Guest_Restaurant @@ -1032,6 +1186,7 @@ /Beam_bridge /BEAM_robotics /Bean + /Beanie_Babies /Bear /Bear-baiting /Beard @@ -1039,37 +1194,47 @@ /Beauty /Beaver /Bed - /Bed_bug /Bed_size /Bedroom /Bee /Bee-eater /Beef + /Beef_brain + /Beef_noodle_soup + /Beef_tongue + /Beef_Wellington + /Beefsteak /Beekeeping /Beer - /Beer_by_region + /Beer_bottle /Beer_pong /Beetle /Begging /Behavior + /Behavioral_economics /Behavioral_neuroscience /Behaviorism + /Behavioural_sciences /Beijing + /Beijing_National_Stadium /Beijing_Subway /Being /Belarus /Belemnoidea /Belfast /Belgium + /Belgrade /Belief /Belief_bias /Belize /Belize_Barrier_Reef /Bell_Canada /Bell_Labs + /Bell_pepper /Belle_de_Boskoop /Belly_dance /Beluga_whale + /Ben_%26_Jerry%27s /Ben_Affleck /Ben_Kingsley /Benchmarking @@ -1085,6 +1250,7 @@ /Berlin /Berlin_Cathedral /Berlin_Wall + /Berm /Bermuda /Bermuda_Triangle /Bernard_Quatermass @@ -1112,8 +1278,10 @@ /Bicycle_Thieves /Big_Bang /Big_Ben + /Big_Bend_National_Park /Big_cat /Big_data + /Big_Mac /Big_O_notation /Big_Star /Big_Ten_Conference @@ -1127,15 +1295,19 @@ /Bill_Nye /Billboard /Billy_Crystal + /Binary_file /Binary_number /Binary_relation /Binary_tree /Binitarianism /Binoculars /Biochemical_engineering + /Biochemist /Biochemistry + /Biodegradation /Biodiesel /Biodiversity + /Biodiversity_loss /Bioethics /Biofeedback /Biofuel @@ -1148,11 +1320,13 @@ /Bioinorganic_chemistry /Biological_anthropology /Biological_engineering + /Biological_life_cycle /Biological_warfare /Biologist /Biology /Biomass /Biomaterial + /Biome /Biomechanics /Biomedical_engineering /Biometrics @@ -1163,6 +1337,7 @@ /Biophysics /Biosensor /BioShock + /BioShock_Infinite /Biosphere /Biostatistics /Biotechnology @@ -1186,8 +1361,10 @@ /Bird_vocalization /Birdwatching /Birmingham + /Birmingham_campaign /Birth_control /Birthday + /Biscayne_National_Park /Biscuit /Bismuth /Bison @@ -1196,6 +1373,9 @@ /Biweekly /Bix_Beiderbecke /Black + /Black_and_white + /Black_bun + /Black_Death /Black_Francis /Black_Hills /Black_hole @@ -1205,17 +1385,25 @@ /Black_rhinoceros /Black_Sea /Black_site + /Blackbeard /BlackBerry + /BlackBerry_10 /BlackBerry_Limited + /BlackBerry_OS + /BlackBerry_World /Blackboard /Blackjack /BlackRock /Blacksmith /Blade_Runner /Blame + /Blasphemy /Blast_Corps /Blimp + /Blizzard + /Blizzard_Entertainment /Block_diagram + /Block_Party_Bash /Blockbuster_LLC /Blockchain /Blog @@ -1223,6 +1411,9 @@ /Blond /Blood_bank /Blood_doping + /Blood_pressure + /Blood_sausage + /Blood_sport /Blood_transfusion /Blood_type /Bloomberg_Businessweek @@ -1237,27 +1428,33 @@ /Blue_Network /Blue_Origin /Blue_whale + /Blueberry /Bluebird /Blues + /Blues_rock /Bluetooth /BMW /BMW_3_Series /BMW_M3 /Boa_constrictor /Board_game + /Boardwalk /Boat /Boat_lift + /Bob_Gurr /Bob_Iger /Bob_Marley /Bobcat /Bobsled_roller_coaster /Bobsleigh + /Bocce /Body_language /Body_mass_index /Body_modification /Body_of_water /Body_piercing /Body_plan + /Body_Wars /Bodyboarding /Bodybuilding /Bodyguard @@ -1272,6 +1469,8 @@ /Boeing_777 /Boerehaat /Boggle + /Bohemian_Rhapsody + /Boiled_egg /Boiling /Boiling_tube /Bok_globule @@ -1279,15 +1478,22 @@ /Bollywood /Bologna /Bomb + /Bomb_disposal /Bombyx_mori /Bomis /Bon_Jovi + /Bone_grafting + /Bone_marrow + /Bongo_drum + /Bonnie_Hunt /Bonobo /Book + /Book_collecting /Book_frontispiece /Book_of_Genesis /Bookbinding /Bookkeeping + /Bookmark /Bookselling /Boolean_algebra /Boomerang @@ -1296,11 +1502,14 @@ /Borat /Borda_count /Bordeaux + /Border + /Border_Collie /Boredom /Borobudur /Boron /Bossaball /Boston + /Boston_Celtics /Boston_College /Boston_Marathon /Boston_Port_Act @@ -1313,17 +1522,23 @@ /Bottlenose_dolphin /Bottling_line /Boulder + /Bounty_hunter /Bow_and_arrow + /Bow_tie /Bowerbird + /Bowhead_whale /Bowl_game + /Bowler_hat /Bowling /Bowls + /Bowsprit /Box /Box_jellyfish /Box_office /Box_Office_Mojo /Box_set /Boxing + /Boxing_Day /Boycott /BP /Bracket @@ -1347,9 +1562,11 @@ /Bravely_Default /Brazil /Brazilian_cruzeiro + /Brazilian_cuisine /Bread /Breadfruit /Breakfast + /Breakfast_cereal /Breaking_Bad /Brewery /Brewing @@ -1357,7 +1574,9 @@ /Brian_Griffin /Brick /Bricker_Amendment + /Bride /Bridge + /Brigadier_general /Brightness /Bristol /Bristol_Channel @@ -1374,6 +1593,7 @@ /British_Museum /British_people /British_Rail + /British_royal_family /British_sitcom /British_Virgin_Islands /Broadband @@ -1386,12 +1606,16 @@ /Bronx_Zoo /Bronze /Bronze_Age + /Brooch /Brood_parasite /Brookfield_Zoo /Brooklyn /Brooklyn_Bridge + /Broth + /Brother_Bear /Brown /Brown_bear + /Brown_hair /Brown_pelican /Brown_spider_monkey /Brown_University @@ -1401,20 +1625,23 @@ /Bruce_Springsteen /Bruce_Sterling /Brunch + /Brussels /Brute-force_attack /Bryophyte + /Bubble_gum /Buckingham_Palace /Buda + /Budapest /Buddhism /Buddhist_art /Budget /Buena_Vista_Street /Buenos_Aires + /Buffer_overflow /Buffet /Bugatti /Bugatti_Veyron /Bugs_Bunny - /Build_to_order /Building /Building_material /Building_science @@ -1426,9 +1653,14 @@ /Bulk_carrier /Bull /Bull_shark + /Bull_Terrier + /Bulldog + /Bullet_time /Bullfighting /Bullying /Bumblebee + /Bumper_cars + /Bumper_sticker /Bungee_jumping /Bungie /Bunk_bed @@ -1447,7 +1679,9 @@ /Bush_v._Gore /Bushshrike /Business + /Business-to-business /Business_cycle + /Business_executive /Business_Insider /Business_magnate /Business_manager @@ -1465,20 +1699,25 @@ /Buzz_Aldrin /Buzz_Lightyear /Buzzard + /BuzzFeed /Bypass_ratio /Byte /Byzantine_Empire /Byzantinism + /C%2B%2B /C-3PO /C-SPAN + /Cabbage /Cable_modem /Cable_television + /Cactus /Cadillac /Cadmium /Caecilian /Caesarean_section /Caesium /Caffeine + /Cairn_Terrier /Cairo /Cairo_Citadel /Cake @@ -1491,10 +1730,14 @@ /Calculus /Caldwell_catalogue /Calendar + /Calgary /Cali_Cartel /Caliber /California - /California_Screamin%27 + /California_cuisine + /California_Gold_Rush + /California_Pizza_Kitchen + /California_Trail /California_Zephyr /Call_centre /Call_of_Duty_2 @@ -1507,13 +1750,12 @@ /Calque /Calvin_Coolidge /Calvin_Klein + /Calzone /Cam_Newton - /Cambarus /Cambodia /Cambodian_riel /Cambrian /Cambrian_explosion - /Cambrian_Series_3 /Cambridge /Camel /Camel_racing @@ -1523,7 +1765,11 @@ /Cameron_Mackintosh /Camp_David /Camp_David_Accords + /Campbell_Soup_Company + /Campfire /Camping + /Can%27t_Feel_My_Face + /Can-can /Canada /Canada_Games /Canada_goose @@ -1534,12 +1780,17 @@ /Canal /Canary_Islands /Cancer + /Candied_fruit /Candle /Candy /Candy_apple + /Candy_cane /Candy_Land + /Cane_toad /Canned_coffee /Cannes_Film_Festival + /Cannibal + /Canning /Cannon /Canoe /Canoe_polo @@ -1552,6 +1803,7 @@ /Caodaism /Capacitor /Capcom + /Cape_Canaveral /Cape_Cod /Cape_Town /Capital_market @@ -1562,6 +1814,7 @@ /Caprimulgiformes /Captain_America /Captain_Britain_Corps + /Captain_EO /Car /Car_bomb /Car_chase @@ -1585,6 +1838,7 @@ /Carcharhiniformes /Card_game /Card_manipulation + /Cardboard /Cardcaptor_Sakura /Cardiac_arrest /Cardiac_muscle @@ -1596,6 +1850,7 @@ /Caret /Cargill /Cargo + /Cargo_aircraft /Cargo_pants /Cargo_ship /Caribbean @@ -1605,31 +1860,42 @@ /Carl_Larsson /Carl_Linnaeus /Carl_Sagan + /Carnival + /Carnival_game /Carnivore /Carousel + /Carousel_of_Progress /Carp + /Carpenter_bee /Carpentry /Carpet /Carriage + /Carrot /Cars_2 /Cars_3 /Cars_Land + /Cars_Quatre_Roues_Rallye /Cars_Toons /Cart /Cartesian_doubt /Carthage + /Cartilage /Cartography /Cartoon /Cartoon_Brew /Cartoon_Network /Cartoonist - /Carved_Stone_Balls /Case_preservation /Case_report + /Casey_Jr._Circus_Train + /Cash + /Cashew /Casino /Caspian_Sea + /Cassette_tape /Cassowary /Cast_iron + /Castaway_Cay /Caster /Castle /Casual_game @@ -1637,6 +1903,7 @@ /Cataloging /Catalysis /Catapult + /Catchphrase /Cate_Blanchett /Catechin /Categorization @@ -1646,13 +1913,15 @@ /Caterpillar /Caterpillar_Inc. /Catfish - /Cathode_ray_tube + /Cathedral /Catholic_Church /Catshark /Cattle /Cauliflower /Causality + /Cause_of_death /Causeway + /Cavalier /Cave /Cave_bear /Cave_painting @@ -1663,15 +1932,19 @@ /CBS /CBS_Corporation /CBS_Films + /CBS_Interactive + /CBS_Morning_News /CBS_News /CD-ROM /CD_player /CD_Projekt + /Cease_and_desist /Ceasefire /Cecilia_Colledge /Cedar_Fair /Ceiling /Celebrity + /Celery /Celestial_spheres /Cell_biology /Cell_culture @@ -1680,6 +1953,9 @@ /Cell_nucleus /Cell_signaling /Cell_wall + /Cellular_agriculture + /Celluloid + /Celsius /Celtic_mythology /Celtic_studies /Celts @@ -1699,6 +1975,7 @@ /Central_Park /Central_processing_unit /Centrifugation + /Centripetal_force /Century /CenturyLink /Ceramic @@ -1713,6 +1990,7 @@ /CFM_International /Chad /Chain + /Chain_store /Chair /Chairlift /Chairman @@ -1740,17 +2018,24 @@ /Charles_Holden /Charlie_Chaplin /Chart + /Charter /Charter_school + /Chat_room /Chatbot /Cheaters /Cheating /Cheating_in_video_games /Check_Point /Checkmate + /Cheddar_cheese /Cheerleading /Cheers /Cheese + /Cheeseburger + /Cheesecake + /Cheesesteak /Cheetah + /Cheetos /Chef /Chelsea_F.C. /Chelsea_Manning @@ -1778,6 +2063,7 @@ /Chemist /Chemistry /Chemosynthesis + /Cheque /Chernobyl_disaster /Cherokee /Cherokee_Nation @@ -1795,6 +2081,9 @@ /Chester_Cathedral /Chevrolet /Chevrolet_Camaro + /Chevrolet_Corvette + /Chewbacca + /Chewing_gum /Chicago /Chicago_Bears /Chicago_Blackhawks @@ -1807,20 +2096,25 @@ /Chichen_Itza /Chick-fil-A /Chicken + /Chickpea /Chicxulub_crater /Chief_creative_officer /Chief_engineer /Chiefdom /Child /Child_development + /Child_prodigy /Childhood + /Children%27s_literature /Children_of_Mana /Chile - /Chimpanzee + /Chili_pepper /China /China_Mobile + /China_Pavilion /China_Telecom /China_Unicom + /Chinatown /Chinchilla /Chinese_art /Chinese_astronomy @@ -1830,16 +2124,23 @@ /Chinese_language /Chinese_mythology /Chinese_New_Year + /Chinese_noodles /Chinese_opera /Chinese_painting /Chinese_room + /Chinese_tea /Chinoiserie /Chinook_Jargon /Chipmunk + /Chipotle_Mexican_Grill /Chivalry /Chlorine /Chloroplast /Chocolate + /Chocolate_bar + /Chocolate_brownie + /Chocolate_cake + /Chocolate_chip_cookie /Choice /Choir /Choking @@ -1851,8 +2152,10 @@ /Chris_Hemsworth /Chris_Pratt /Christian_Bale + /Christian_pilgrimage /Christianity /Christmas + /Christmas_ornament /Christoph_Waltz /Christopher_Nolan /Chroma_key @@ -1861,18 +2164,29 @@ /Chromecast /Chromium /Chromosome + /Chronicle /Chrono_Cross /Chrono_Trigger /Chronobiology + /Chronology /Chrysler /Chrysler_Building + /Chuck_E._Cheese%27s /Church_music + /Churro /Cicero /Cider /Cider_apple /Cigarette + /Cigarette_card + /Cigarette_case + /Cigarette_holder + /Cigarette_pack + /Cin%C3%A9Magique + /Cincinnati /Cincinnati_Reds /Cinco_de_Mayo + /Cinderella /Cinderella_Castle /Cinema_of_Europe /Cinema_of_Italy @@ -1880,13 +2194,19 @@ /CinemaScore /Cinematographer /Cinematography + /Cinnabon + /Cinnamon /Cipher /Circle + /Circle-Vision + /Circle-Vision_360%C2%B0 /Circle_7_Animation /Circuit_breaker /Circuit_design /Circuit_diagram + /Circuit_switching /Circulatory_system + /Circumference /Circus /Circus_clown /Circus_Maximus @@ -1901,17 +2221,20 @@ /Citizenship /Citric_acid_cycle /City + /City-building_game /City-state /City_block /City_comedy /Civic_center /Civics + /Civil_aviation /Civil_disobedience /Civil_engineer /Civil_engineering /Civil_liberties /Civil_procedure /Civil_resistance + /Civil_rights_movement /Civil_society /Civil_township /Civil_war @@ -1923,6 +2246,8 @@ /Clapotis /Clarinet /Class_conflict + /Classic_car + /Classic_Mac_OS /Classical_antiquity /Classical_Chinese /Classical_economics @@ -1934,7 +2259,10 @@ /Classical_planet /Classics /Clause + /Claw + /Claw_crane /Clay + /Clay_animation /Clean_technology /Clean_Water_Act /Clement_Greenberg @@ -1950,8 +2278,10 @@ /Climate_change /Climate_of_Antarctica /Climatology + /Climax_community /Clinical_pathology /Clinical_trial + /Clio /Cloak /Clock /Clockwork @@ -1965,6 +2295,7 @@ /Cloud_gaming /Cloud_Strife /Clown + /Club_33 /Club_Penguin /CM_Punk /CMYK_color_model @@ -1974,9 +2305,9 @@ /Cnidaria /CNN /CNN_International - /CNNMoney /Coaching /Coal + /Coal_mining /Coal_tit /Coalbed_methane /Coalition @@ -1992,6 +2323,10 @@ /Coccinellidae /Cockatoo /Cocktail + /Cocktail_glass + /Cocoa_bean + /Cocoa_butter + /Cocoa_solids /Coconut /Coconut_milk /Coconut_oil @@ -2002,12 +2337,11 @@ /Code_of_law /Coding_theory /Codling_moth + /Coercion /Coffee /Coffeehouse /Coffeemaker /Cogeneration - /Cogito_ergo_sum - /Cognition /Cognitive_bias /Cognitive_map /Cognitive_science @@ -2019,13 +2353,16 @@ /Cold_cathode /Cold_dark_matter /Cold_Feet + /Cold_Stone_Creamery /Cold_War + /Coldplay /Colin_McCahon /Colin_McGinn /Collaboration /Collaborative_software /Collage /Collectable + /Collectible_card_game /Collecting /Collective_intelligence /Collective_memory @@ -2044,8 +2381,10 @@ /Colonization /Color /Color_blindness + /Color_gradient /Color_television /Color_theory + /Color_vision /Colorado /Colorado_Avalanche /Colorado_River @@ -2056,15 +2395,14 @@ /Columbia_Records /Columbia_River /Columbia_University - /Columbian_Exchange /Columbian_mammoth /Columbidae + /Columbus_Day /Column /Combat_helmet /Combat_sport /Combinatorics /Combine_harvester - /Combined_cycle /Combined_sewer /Combustion /Combustor @@ -2085,12 +2423,13 @@ /Comma /Commander-in-chief /Commander_Keen + /Commedia_dell%27arte /Commerce + /Commercial_art /Commodity /Commodore_64 /Common_cold /Common_Era - /Common_knowledge /Common_law /Common_nightingale /Common_ostrich @@ -2101,6 +2440,8 @@ /Commonwealth_realm /Communication /Communication_studies + /Communications_satellite + /CommuniCore /Communism /Communist_party /Communitarianism @@ -2114,7 +2455,6 @@ /Commuter_rail /Commuting /Compact_car - /Compact_Cassette /Compact_disc /Company /Compaq @@ -2126,6 +2466,7 @@ /Competition /Competition_law /Competitive_dance + /Compilation_album /Compiler /Complement_system /Complex_analysis @@ -2164,6 +2505,8 @@ /Computer_security /Computer_simulation /Computer_Space + /Computer_terminal + /Computer_virus /Computer_vision /Computer_worm /Computing @@ -2185,10 +2528,12 @@ /Concrete /Concussion /Condensation + /Condensed_milk /Condiment /Condor /Conducting /Coney_Island + /Confectionery /Confidence_trick /Confidentiality /Confirmation_bias @@ -2201,24 +2546,33 @@ /Confucianism /Confucius /Confusion + /Congaree_National_Park /Congestion_pricing /Congress_Poland /Conjecture /Connect_Four /Connecticut + /Connecticut_River /Connectionism + /Connective_tissue /Conscience /Consciousness + /Conscription + /Conservation_easement /Conservatism /Consistency /Console_game + /Constantine_VIII /Constitution /Constitutional_law /Construction /Construction_puzzle /Construction_set /Consultant + /Consumer /Consumer_electronics + /Consumer_organization + /Consumer_price_index /Consumerism /Contact_juggling /Container_ship @@ -2226,11 +2580,12 @@ /Contemporary_art /Contemporary_philosophy /Contempt + /Content_delivery_network /Contentment /Contextualism - /Contiguity /Continent /Continental_Army + /Continental_Congress /Continental_crust /Continental_drift /Continental_Europe @@ -2244,6 +2599,8 @@ /Control_system /Control_theory /Convection + /Convenience_food + /Convenience_store /Conventional_weapon /Conversion_therapy /Convertible @@ -2253,6 +2610,7 @@ /Cookie /Cooking /Cooking_apple + /Cooking_Channel /Cooking_show /Cookware_and_bakeware /Cool_Runnings @@ -2278,6 +2636,10 @@ /Coregonus /Coriolis_force /Cormorant + /Corn_starch + /Corn_syrup + /Corn_tortilla + /Corned_beef /Cornell_University /Corporate_group /Corporate_jargon @@ -2299,13 +2661,17 @@ /Costco /Costume /Costume_design + /Costumed_character /Cotinga /Cotton /Cotton_mill /Couch /Cougar + /Coulomb%27s_law /Council_of_Europe /Count_Dracula + /Counterculture + /Counterfeit_money /Counterpoint /Counting /Country @@ -2317,27 +2683,36 @@ /County_Championship /County_seat /Coup_d%27%C3%A9tat + /Couplet /Courage /Courier /Courser /Court_clerk /Court_of_Chancery + /Courtesy_name + /Courthouse + /Cousin_marriage /Covalent_bond /Covered_bridge /Cowboy + /Cowboy_hat /Coyote /Coypu /CPU_cache + /Cr%C3%AApe /Crab /Crab-plover /Cracidae /Crack_cocaine /Crackdown + /Cracker_Jack /Craft /Craig_Federighi /Craigslist + /Cramp /Crane_fly /Craniate + /Cranium_Command /Craps /Crash_Team_Racing /Cray @@ -2345,6 +2720,7 @@ /Crayola /Crayon /Cream + /Cream_cheese /Creative_director /Creative_synthesis /Creative_writing @@ -2372,13 +2748,16 @@ /Croatia /Crocheted_lace /Crocodile + /Crocodilia + /Croissant /Crop /Crop_rotation /Crop_yield /Croquet + /Croquette /Cross-cultural - /Cross-platform /Crossbow + /Crosswalk /Crossword /Crow /Crowd_simulation @@ -2392,6 +2771,7 @@ /Cruiser /Crumble /Crusades + /Crush%27s_Coaster /Cryobiology /Cryogenian /Cryogenics @@ -2422,10 +2802,11 @@ /Cuckoo /Cuckoo_roller /Cuckooshrike + /Cucumber /Cue_sports /Cuisine - /Culinary_art /Cult_film + /Cult_following /Cultivar /Cultural_area /Cultural_artifact @@ -2433,6 +2814,7 @@ /Cultural_evolution /Cultural_genocide /Cultural_geography + /Cultural_heritage /Cultural_history /Cultural_icon /Cultural_identity @@ -2442,6 +2824,7 @@ /Cultural_relativism /Cultural_Revolution /Cultural_studies + /Cultural_tourism /Cultural_turn /Culture /Culture_change @@ -2451,18 +2834,23 @@ /Culture_of_Poland /Cultured_meat /Culturology + /Cupcake /Curator + /Curb_Your_Enthusiasm /Curd /Curiosity /Curling /Currency - /Currency_detector /Currency_symbol /Curriculum /Curry + /Curry_powder /Curse /Curved_mirror /Cusco + /Custard + /Cutout_animation + /Cutscene /Cyanide /Cyberattack /Cyberbullying @@ -2486,6 +2874,7 @@ /Czech_handball /Czech_Republic /Czechoslovakia + /Dachshund /Dada /Daffy_Duck /Daft_Punk @@ -2493,6 +2882,7 @@ /Dairy /Dairy_farming /Dairy_product + /Dairy_Queen /Daisy_Ridley /Dalai_Lama /Dallas @@ -2502,9 +2892,12 @@ /Dam_failure /Damageplan /Damascus + /Damsel-in-distress /Dance + /Dance_music /Dance_notation /Dance_pad + /Dancing_with_the_Stars /Daniel_Boone /Daniel_Day-Lewis /Daniel_Dennett @@ -2518,6 +2911,8 @@ /Dark_matter /Dark_Ranger /Dark_ride + /Dark_Souls + /Dark_tourism /Dark_web /DARPA /Darter @@ -2529,6 +2924,7 @@ /Data /Data_analysis /Data_breach + /Data_center /Data_compression /Data_flow_diagram /Data_link_layer @@ -2540,8 +2936,10 @@ /Data_storage /Data_structure /Data_transmission + /Data_type /Data_visualization /Database + /Dateline_NBC /Dating_sim /Daughter /Davao_Gulf @@ -2552,17 +2950,25 @@ /Day_of_the_Dead /Daylight_saving_time /Days_of_Future_Past + /DC_animated_universe /DC_Comics + /DC_Extended_Universe + /De-extinction /De_jure /De_Stijl /Dead_Poets_Society - /Deadline.com + /Deadpan /Deadpool + /Deadpool_2 /Deaf_basketball + /Death /Death_Eater /Death_on_the_Rock + /Debt /Debugging + /Decade /Decadence + /Decapitation /Decathlon /December /Deciduous @@ -2582,6 +2988,7 @@ /Defibrillation /Definition /Definition_of_planet + /Deflation /Deforestation /Degree_symbol /Deindustrialization @@ -2592,12 +2999,15 @@ /Delegate /Delhi /Delicacy + /Delicatessen /Delivery_drone /Dell /Deloitte /Delphi /Delta_Air_Lines + /Delta_Dreamflight /Delta_Works + /Demand /Demand_response /Demigod /Democracy @@ -2607,6 +3017,7 @@ /Demon /Demosthenes /Dendrite + /Denial-of-service_attack /Denmark /Density /Dental_hygienist @@ -2620,6 +3031,7 @@ /Deposit_account /Depth_of_field /Deregulation + /Derivative /Dermatology /Descriptive_ethics /Desert @@ -2627,6 +3039,7 @@ /Design /Design_engineer /Design_of_experiments + /Design_rationale /Design_thinking /Designated_hitter /Designer @@ -2646,6 +3059,7 @@ /Detroit_Red_Wings /Deus_ex_machina /Deuterostome + /Devaluation /Devanagari /Developed_country /Developing_country @@ -2656,11 +3070,14 @@ /DHL_Express /Diacritic /Diagram + /Dial-up_Internet_access /Dialect /Dialectic + /Dialysis /Diamond /Diane_Keaton /Diarrhea + /Diary /Diaspora /Dice /Dictator @@ -2672,13 +3089,16 @@ /Die-cast_toy /Die_Another_Day /Die_casting + /Die_Hard /Diesel_engine + /Diesel_fuel /Diesel_locomotive /Diet_food /Dietary_fiber /Dietary_supplement /Dieting /Dietitian + /Differential_calculus /Differential_equation /Differential_geometry /Differential_topology @@ -2700,6 +3120,7 @@ /Digital_radio /Digital_recording /Digital_Revolution + /Digital_rights /Digital_signature /Digital_television /Digitization @@ -2707,6 +3128,7 @@ /Dim_sum /Dimension /Dimension_Films + /Diner /Dingo /Dining_room /Dinner @@ -2714,15 +3136,19 @@ /Dinosaur /Diode /Dioecy + /Diploma /Diplomacy /Diplomat /Dipole_antenna /Dipper + /Dipping_sauce /Diptych /Dire_wolf /Direct-to-video /Direct_current + /Direct_democracy /DirecTV + /DirectX /Disappointment /Disaster /Disc_golf @@ -2741,33 +3167,45 @@ /Dish_Network /Dishonored /Dishwasher + /Disinformation + /Disk_storage /Disney_Channel /Disney_Cruise_Line + /Disney_Dream + /Disney_Dreams! + /Disney_Fantasy + /Disney_in_the_Stars + /Disney_Junior + /Disney_on_Parade /Disney_Renaissance + /Disney_Research + /Disney_Resort_Line + /Disney_Skyliner /Disney_Springs /Disney_Store + /Disney_Transport /Disney_utilidor_system /Disney_Vacation_Club /Disney_Wonder + /Disney_XD /Disneyland /Disneyland_Paris /Disneyland_Railroad /Disneyland_Resort - /DisneyToon_Studios /Dispatcher /Display_case /Display_device /Display_resolution /Disposition + /Dissection /Distance_education /Distancing_effect /Distillation - /Distilled_beverage /Distraction /Distributive_property /Ditto_mark - /Diving /Diving_bell + /Divinity /DIY_ethic /DJ_Hero /Django_Unchained @@ -2781,9 +3219,13 @@ /DNA_sequencing /Do_the_Right_Thing /Doctor_Doom + /Doctor_of_Medicine + /Doctor_of_Philosophy /Doctor_Strange /Doctor_Who + /Doctorate /Doctrine + /Document /Documentary_film /Documentation /Dodge @@ -2792,6 +3234,11 @@ /Dodgeball /Dodo /Dog + /Dog_breed + /Dog_breeding + /Dog_food + /Dogma + /Dogs_in_warfare /Dolby_3D /Dolby_Theatre /Doll @@ -2801,11 +3248,15 @@ /Dolphin /Dolphinarium /Domain_knowledge + /Domain_name /Domain_Name_System /Dome + /Dome_of_the_Rock + /Domed_city /Domestic_market /Domestic_pig /Domestic_robot + /Domestic_turkey /Domestic_worker /Domestic_yak /Domestication @@ -2813,7 +3264,10 @@ /Dominican_Republic /Domino%27s_Pizza /Dominoes + /Don%27t_Stop_Believin%27 + /Don%27t_Stop_Me_Now /Don_Ameche + /Donald%27s_Boat /Donald_Duck /Donald_Knuth /Donald_O._Hebb @@ -2825,15 +3279,18 @@ /Doomsday_device /Door /Dopamine + /Dora_the_Explorer /Doritos /Dormouse /Dorothy_Parker + /DOS /Dot-com_bubble /Dota_2 /Double-track_railway /Double_bind /Doubt /Dough + /Doughnut /Douglas_Adams /Douglas_DC-7 /Douglas_DC-8 @@ -2842,32 +3299,40 @@ /Dowel /Down_syndrome /Downburst + /Download /Downton_Abbey /Downtown + /Downtown_Disney /Dowry + /Doxing /Dr._Dre /Draco_Malfoy + /Dracula /Draft_evasion /Dragon /Dragon_Ball /Dragon_Quest /Dragonfly + /Drainage /Drakengard /Drama /Dramaturgy /Drawbridge /Drawing /Dream + /Dream-Along_With_Mickey /Dreamcast /DreamWorks /DreamWorks_Animation /Dredd /Dress + /Dried_fruit /Drift_ice /Drilling /Drilling_rig /Drillship /Drink + /Drink_can /Drinking /Drinking_water /Drive-in_theater @@ -2879,6 +3344,7 @@ /Drug_cartel /Drum /Drummer + /Dry_county /Dry_ice /Dryland_farming /Dubai @@ -2893,6 +3359,9 @@ /Dumpling /Dumpster_diving /Dunder_Mifflin + /Dungeons_%26_Dragons + /Dunkin%27_Donuts + /Dunkirk /Duns_Scotus /Durban /Dust @@ -2905,6 +3374,7 @@ /DVD /DVD_player /Dwarf_planet + /Dwarfism /Dwayne_Johnson /Dwight_D._Eisenhower /Dwyane_Wade @@ -2921,7 +3391,9 @@ /E-democracy /E-reader /E_ticket + /EA_DICE /Eagle_Eye + /Earful_Tower /Early_Middle_Ages /Early_music /Earth @@ -2929,11 +3401,14 @@ /Earth%27s_rotation /Earth_Day /Earth_mass + /Earth_oven /Earth_science /Earth_Summit /EarthBound /Earthquake + /Earthrise /Earthworm + /Earworm /East_Africa /East_Asia /East_Asian_Games @@ -2946,6 +3421,7 @@ /Easter_Bunny /Easter_Island /Eastern_Bloc + /Eastern_Europe /Eastern_grey_kangaroo /Eastern_Hemisphere /Eastern_philosophy @@ -2956,10 +3432,13 @@ /Eating /Eating_disorder /EBay + /Ebenezer_Scrooge /Ebola_vaccine + /Ebony /Eclipse /Ecliptic /Ecodesign + /Ecological_collapse /Ecological_niche /Ecological_pyramid /Ecology @@ -2982,6 +3461,7 @@ /Ecosystem /Ecosystem_ecology /Ecotone + /Ecotourism /Ecovillage /Ecuador /Edaphology @@ -3021,21 +3501,27 @@ /Effectiveness /Efficacy /Efficiency + /Efficient_energy_use /Egalitarianism /Egg /Egg_as_food + /Egg_white + /Eggnog /Eggplant /Egremont_Russet /Egypt /EgyptAir /Egyptian_Arabic + /Egyptian_Army /Egyptian_astronomy /Egyptian_calendar /Egyptian_Hall /Egyptian_hieroglyphs /Egyptian_Museum /Egyptian_mythology + /Egyptian_National_Police /Egyptian_pyramids + /Egyptian_temple /Eidetic_memory /Eiffel_Tower /Einsteinium @@ -3043,24 +3529,28 @@ /Ejecta /El_Al /El_Lissitzky + /El_Rio_del_Tiempo /El_Salvador /Election /Electoral_district + /Electoral_system /Electric_arc /Electric_car /Electric_charge + /Electric_current /Electric_generator /Electric_guitar + /Electric_heating /Electric_light /Electric_motor /Electric_power - /Electric_shock - /Electric_stove /Electric_utility + /Electrical_connector /Electrical_energy /Electrical_engineering /Electrical_grid /Electrical_network + /Electrical_telegraph /Electrical_wiring /Electricity /Electricity_generation @@ -3069,22 +3559,24 @@ /Electromagnetism /Electromechanics /Electron - /Electron_hole /Electron_mobility /Electronegativity /Electronic_Arts /Electronic_circuit /Electronic_component + /Electronic_dance_music /Electronic_engineering /Electronic_game /Electronic_Games /Electronic_music /Electronic_paper /Electronic_waste + /ElecTRONica /Electronics /Electronvolt /Elementary_algebra /Elementary_particle + /Elementary_school /Elephant /Elephant_seal /Elevator @@ -3105,12 +3597,13 @@ /Elvis_Presley /Emacs /Email + /Email_client /Embarrassment /Embedded_system /Emberizidae /Emblem - /Embryogenesis /Embryology + /Emerald /Emergence /Emergency_medicine /Emergency_service @@ -3128,21 +3621,25 @@ /Empathy /Emperor_penguin /Empire + /Empire_of_Japan /Empire_State_Building /Empirical_evidence /Empirical_research /Empiricism /Employment + /Empowerment /Emu + /Emulator /Enceladus + /Enchanted_Tiki_Room /Enclave_and_exclave /Encryption /Encyclopedia + /End_time /Endangered_species /Endemism /Endocrinology /Endodontics - /Endothermic_process /Energy /Energy_conservation /Energy_density @@ -3182,26 +3679,34 @@ /Entity /Entomology /Entomophily + /Entrapment /Entrepreneurship /Entropy /Environmental_chemistry + /Environmental_history + /Environmental_issue /Environmental_law + /Environmental_protection /Environmental_science /Environmental_studies /Environmentalism /Environmentalist + /Environmentally_friendly /Enzyme /Enzyme_assay /Enzyme_inhibitor /Eon_Productions /Epcot /Epcot_Resort_Area + /Ephemera + /Epic_film /Epic_Games /Epic_poetry /Epicuticular_wax /Epidemiology /Epigenetics /Epiphyte + /Episode /Epistemology /Epithet /Epsilon_Eridani @@ -3211,8 +3716,10 @@ /Equinox /Eraserhead /Eratosthenes + /Erector_Set /Ergodic_theory /Eric_Bana + /Ericsson_Globe /Erie_Railroad /Ernest_Hemingway /Ernie_Banks @@ -3224,18 +3731,18 @@ /Escape_velocity /Escapism /Escapology + /Eskimo /Espionage /ESPN + /ESPN2 /ESPN_Inc. /ESPN_on_ABC - /ESports + /ESPN_Zone /Essentialism /Essex /Estimation /Estonia - /Estrildid_finch /Estuary - /Etch_A_Sketch /Eternal_youth /Ethan_Hawke /Ethanol @@ -3286,27 +3793,32 @@ /Evaporation /Eve_Online /Everglades + /Everglades_National_Park /Evergreen + /Every_Breath_You_Take /Everyday_life /Everything /Evidence /Evolution /Evolution_of_birds /Ex_parte_Crow_Dog + /Exaggeration /Exchange_rate /Exchange_value /Exciton /Exclamation_mark + /Executable /Executive_functions /Executive_order /Executive_producer - /Exercise /Exercise_machine /Exhibit_design /Exhibition /Existence /Existentialism + /Exoplanet /Exorcism + /Exoskeleton /Exothermic_reaction /Expansion_pack /Expected_value @@ -3325,12 +3837,17 @@ /Exploration_of_the_Moon /Exploration_of_Uranus /Exploratorium - /Explosive_material + /Explosive + /Exponential_growth /Exponentiation + /Export + /Expressionism /Extended_family + /Extended_play /Extensive_farming /Extinction /Extinction_event + /Extraterrestrial_life /Extreme_poverty /Extreme_sport /Exxon @@ -3349,19 +3866,24 @@ /Fair_trade /Fair_use /Fairlie_locomotive + /Fairmount_Park /FairTax /Fairy /Fairy_Tail /Fairy_tale /Faith + /Fake_news /Falcon /Falcon_9 /Falcon_Heavy /Falconidae /Falconry + /Fall_of_Antwerp + /Fall_of_Constantinople /Fall_Out_Boy /Fallacy /Fallibilism + /Fallingwater /Fallout_3 /Fallout_4 /False_dilemma @@ -3375,13 +3897,16 @@ /Family_Guy /Family_law /Famine + /Fan_death /Fan_film /Fandom /Fantasmic! /Fantastic_Four /Fantasy /Fantasy_film + /Fantasy_in_the_Sky /Fantasyland + /Fantasyland_Theater /FAQ /Far-right_politics /Farce @@ -3394,26 +3919,36 @@ /Fashion_design /Fashion_doll /Fashion_show + /Fast_%26_Furious_6 /Fast_Five /Fast_food + /Fast_food_restaurant + /FastPass /Fat + /Fatal_Attraction /Father /Fatty_acid /Fault_scarp + /Fault_tolerance /Fauna /Fauvism + /Fax /FC_Barcelona /Fear /Fear_the_Walking_Dead /Feather + /Feather_pen /Feature_film /February + /Federal_Hall /Federal_judge /Federal_law /Federal_Power_Act + /Federal_prison /Federal_republic + /Federal_Reserve /Federal_Reserve_Act - /Federal_Reserve_System + /Federal_Reserve_Note /Federalist_Party /FedEx /Feedback @@ -3430,6 +3965,7 @@ /Ferrari /Ferret /Ferris_Wheel + /Ferris_wheel /Ferry /Fertilisation /Fertility @@ -3437,6 +3973,7 @@ /Festival /Feudalism /Fiat_Automobiles + /Fiat_money /Fiber /Fiber_bundle /Fibonacci_number @@ -3464,6 +4001,7 @@ /File_transfer /File_viewer /FileMaker + /Filing_cabinet /Filipinos /Film /Film_adaptation @@ -3477,6 +4015,7 @@ /Film_genre /Film_industry /Film_noir + /Film_poster /Film_preservation /Film_producer /Film_score @@ -3486,6 +4025,7 @@ /Film_studio /Film_theory /Filmmaking + /Fin_whale /Final_Cut_Pro /Final_Cut_Studio /Final_Fantasy @@ -3501,9 +4041,11 @@ /Finding_Nemo /Fine_art /Fine_motor_skill + /Fineness /Finger_wave /Finite_geometry /Finland + /Firaxis_Games /Fire /Fire-tube_boiler /Fire_breathing @@ -3511,9 +4053,12 @@ /Fire_ecology /Fire_Emblem /Fire_Emblem_Fates + /Fire_extinguisher + /Fire_lookout_tower /Fire_performance /Fire_protection /Fire_safety + /Fire_ship /Firearm /Firefighter /Firefighting @@ -3522,7 +4067,9 @@ /Fireplace /Fireworks /Firmament + /Firmware /First-person_shooter + /First_aid /First_language /First_Nations /First_ScotRail @@ -3530,25 +4077,34 @@ /Fiscal_policy /Fiscal_year /Fish + /Fish_cracker /Fish_emulsion /Fish_farming + /Fish_paste + /Fish_sauce /Fisherman /Fishery /Fishing /Fishing_industry + /Fishing_rod /Fishing_vessel /Five_Eyes + /Five_Guys + /Fixed-wing_aircraft /Fjord + /FLAC /Flag /Flag_football /Flamingo /Flannel + /Flash_animation /Flash_Gordon /Flash_memory + /Flash_Video /Flashdance /Flashforward /Flashtube - /Flat_panel_display + /Flatbread /Flattop /Flavan-3-ol /Flavonoid @@ -3558,32 +4114,42 @@ /Flea_circus /Fleetwood_Mac /Flight + /Flight_attendant + /Flight_Circle /Flight_controller /Flight_engineer /Flight_simulator + /Flight_to_the_Moon + /Flik%27s_Flyers /Flint /Flint_water_crisis + /Flip_book /Flood /Floodgate + /Floodplain /Floor /Floor_hockey + /Floor_plan /Floppy_disk /Flora /Florence + /Florence_Cathedral /Flores /Florida /Florida_Territory /Florin_sign + /Flour /Flowchart /Flower /Flowering_plant - /Flowerpecker /Floyd_Mayweather_Jr. + /Fluid /Fluid_dynamics /Fluid_mechanics /Fluorescent_lamp /Fluoride /Fluorine + /Flush_toilet /Fly /Fly_system /Flying_ace @@ -3600,8 +4166,10 @@ /Folk_music /Folk_rock /Folklore + /Folly /Font /Fontainebleau + /Foo_Fighters /Food /Food_allergy /Food_bank @@ -3611,9 +4179,11 @@ /Food_energy /Food_engineering /Food_industry - /Food_photography + /Food_Network /Food_presentation /Food_preservation + /Food_processing + /Food_Rocks /Food_science /Food_security /Food_waste @@ -3628,13 +4198,16 @@ /Forbidden_fruit /Force /Forced_perspective + /Ford%27s_Theatre /Ford_Model_T /Ford_Motor_Company /Ford_Mustang /Forecasting + /Foreign_exchange_market /Foreign_language /Foreign_minister /Foreign_Policy + /Forensic_Files /Forensic_pathology /Forensic_science /Foreshadowing @@ -3651,16 +4224,24 @@ /Formula /Formula_One /Forrest_Gump + /Fort_Caroline + /Fort_McHenry + /Fort_Niagara + /Fort_Niagara_State_Park + /Fort_Sumter /Fort_Vancouver /Forth_Bridge /Fortran - /Fortress_of_Klis + /Fortune_cookie /Forward_pass /Fossil /Fossil_fuel /Foster_and_Partners + /Foster_the_People /Foundationalism /Fountain + /Fountain_of_Nations + /Fountain_pen /Four-player_chess /Four_color_theorem /Fourier_analysis @@ -3674,6 +4255,7 @@ /Fox_Sports_Networks /Foxconn /Fractal + /Frame_rate /France /Frances_Langford /Franchising @@ -3689,37 +4271,47 @@ /Freaks /Free-to-play /Free_content + /Free_fall /Free_license /Free_market /Free_software /Free_trade /Free_will + /Freedom_of_information /Freedom_of_speech /Freelancer /Freemasonry /Freemium + /Freestyle_wrestling /Freethought /Freeware /Freeze-drying /Freight_transport /French_Alps + /French_and_Indian_War /French_Armed_Forces /French_Army + /French_cuisine /French_franc /French_fries /French_Guiana /French_language /French_Open /French_people + /French_Quarter /French_Revolution + /French_Riviera /French_Union /French_wine /Frequency /Fresco /Fresh_water /Freyr + /Friction /Frida_Kahlo /Friday + /Fried_chicken + /Fried_onion /Fried_pickle /Friendly_society /Friends @@ -3727,26 +4319,30 @@ /Frigatebird /Frigg /Frisbee + /Frito-Lay /Fritz_Zwicky /Frog - /Frogmouth + /Frogger /From_Hell /Frontierland /Frown + /Frozen_Ever_After /Frozen_food + /Fructose /Frugality /Fruit - /Fruit_picking /Fruit_preserves /Fruit_tree /Fruit_tree_pruning + /Fruitcake /Frying + /Fudge /Fuel /Fuel_cell /Fuel_injection /Fuel_oil /Fujitsu - /Full-text_search + /Full-rigged_ship /Full_moon /Full_stop /Fullerene @@ -3760,10 +4356,13 @@ /Funicular /Funk /Funny_animal + /Fur /Fur_seal /Furious_7 /Furniture /Fusion_cuisine + /Fusion_power + /Futurama /Future /Future_history /Futures_studies @@ -3775,6 +4374,7 @@ /Gag_cartoon /Gagarino /Gaia_hypothesis + /Gal_Gadot /Galaxy /Galaxy_cluster /Galileo_Galilei @@ -3790,10 +4390,14 @@ /Game_Boy_Micro /Game_Center /Game_controller + /Game_demo /Game_design + /Game_engine /Game_Gear /Game_mechanics + /Game_of_chance /Game_of_Thrones + /Game_reserve /Game_show /Game_studies /Game_theory @@ -3820,6 +4424,7 @@ /Garden_of_Eden /Gardening /Garfield + /Garlic /Garmin /Garry_Schyman /Gary_Oldman @@ -3840,7 +4445,10 @@ /Gear /Gears_of_War /Geek + /Gel + /Gelatin /Gemology + /Gemstone /Gender /Gender_dysphoria /Gender_identity @@ -3851,6 +4459,8 @@ /Gene_therapy /General_aviation /General_Electric + /General_Foods + /General_Mills /General_Motors /General_officer /General_order @@ -3874,6 +4484,7 @@ /Genre /Genre_fiction /Gensler + /Gentrification /Genus /Geocaching /Geocentric_model @@ -3920,11 +4531,15 @@ /Gerald_Ford /Gerard_Butler /Geriatrics + /German_colonial_empire + /German_East_Africa /German_Empire /German_gold_mark /German_idealism /German_language + /German_Shepherd /German_studies + /German_submarine_U-331 /German_wine /Germanic_peoples /Germanium @@ -3935,6 +4550,8 @@ /Gestapo /Get_Out /Gettier_problem + /Gettysburg_Address + /Gettysburg_Campaign /Geyser /Ghana /Ghanaian_cedi @@ -3945,25 +4562,39 @@ /Giant_anteater /Giant_panda /Gibbs_free_energy + /Gibraltar + /GIF + /Gift_economy + /Gift_shop /Gift_tax /Gifted_education /Gigabyte /Gila_monster /Gilbert_Ryle /Gilded_Age + /Gill + /GIMP /Gin + /Ginger + /Gingerbread /Giraffe /Girder_bridge /GitHub /Giuseppe_Peano + /Giza + /Giza_pyramid_complex /Glacial_period /Glacier /Glaciology /Gladiator + /Gland /Glareolidae /Glasgow /Glass + /Glass_milk_bottle + /Glasses /Glitch + /Global_catastrophic_risk /Global_city /Global_illumination /Global_politics @@ -3982,7 +4613,6 @@ /Glossary_of_ichthyology /Glossary_of_Islam /Glossary_of_tennis_terms - /Glossary_of_topology /Gloster_Meteor /Glove /Glucose @@ -3990,7 +4620,6 @@ /Glycerol /Glycolysis /Gmail - /Gnatcatcher /Gnosticism /GNU_Core_Utilities /GNU_Debugger @@ -3998,6 +4627,8 @@ /Go-kart /Goal /Goat + /Goat_meat + /Goatee /Goblin /God /Godsmack @@ -4005,35 +4636,49 @@ /Gold /Gold_leaf /Gold_medal + /Gold_mining /Gold_reserve /Gold_rush /Gold_standard + /Golden_Age_of_Radio /Golden_apple /Golden_Delicious + /Golden_Dreams /Golden_eagle /Golden_Gate /Golden_Gate_Bridge + /Golden_Gate_Park /Golden_Globe_Award /Golden_Horseshoe_Saloon /Golden_Lion /Golden_ratio + /Golden_Retriever /Golden_State_Warriors + /Golden_Zephyr /GoldenEye /Goldfish /Goldman_Sachs /Golf + /Golf_Channel + /Golf_club /Golgi_apparatus /Gomphidae + /Gondola_lift /Gondwana /Gonzaga_Bulldogs /Good_and_evil /Good_Friday + /Good_Morning_America /Goods /Goofy + /Goofy%27s_Sky_School + /Googie_architecture /Google /Google_Assistant /Google_Books /Google_Chrome + /Google_Doodle + /Google_Drive /Google_Earth /Google_Fiber /Google_Finance @@ -4041,6 +4686,7 @@ /Google_Maps /Google_News /Google_Search + /Googly_eyes /Goose /Goosebumps /GoPro @@ -4048,10 +4694,12 @@ /Gorilla /Goris /Gospel_music + /Gothic_architecture /Gothic_art /Gottlob_Frege /Gourmet /Government + /Government_agency /Government_debt /Government_spending /Governor @@ -4065,31 +4713,39 @@ /Grammy_Award /Granary /Grand_Canyon + /Grand_Central_Terminal /Grand_Chess /Grand_jury /Grand_Ole_Opry + /Grand_Palais /Grand_Theft_Auto /Grand_Theft_Auto_IV /Grand_Theft_Auto_V + /Grand_Tour /Grandparent /Granite /Granny_Smith + /Grant%27s_Tomb /Grape /Graph_theory /Graphene /Graphic_arts /Graphic_design + /Graphic_designer /Graphic_novel /Graphical_user_interface /Graphics /Graphing_calculator /Graphite + /Grappling_hook + /Grass_jelly /Grasshopper /Grassland /Gravenstein /Gravitational_wave /Gravity - /Gray_wolf + /Gravy + /Gray_whale /Great_Awakening /Great_Barrier_Reef /Great_Britain @@ -4098,6 +4754,7 @@ /Great_Expectations /Great_Lakes /Great_Leap_Forward + /Great_Mosque_of_Mecca /Great_Plains /Great_Pyramid_of_Giza /Great_Recession @@ -4127,6 +4784,7 @@ /Green_Hill_Zone /Green_politics /Green_roof + /Green_sea_turtle /Green_tea /Green_waste /Greenhouse @@ -4134,23 +4792,32 @@ /Greenhouse_gas /Greenland /Greenpeace + /Greeting_card /Gregorian_calendar /Grenada /Grenade /Grey /Grey%27s_Anatomy /Grey_goo + /Greyhound_racing /Grid_computing /Grilling /Grim_Fandango + /Grimms%27_Fairy_Tales + /Gristmill /Grizzly_bear + /Grizzly_River_Run /Grocery_store /Gross_domestic_product /Grotesque + /Ground_beef + /Ground_meat /Ground_sloth /Ground_state /Groundhog + /Groundhog_Day /Groundwater + /Groundwater_recharge /Group_cohesiveness /Group_dynamics /Group_theory @@ -4165,9 +4832,11 @@ /Guam /Guanaco /Guant%C3%A1namo_Bay + /Guard_dog /Guatemala /Guerrilla_warfare /Guglielmo_Marconi + /Guide_book /Guild /Guild_Wars /Guillemet @@ -4189,6 +4858,9 @@ /Gullibility /Gully /Gun + /Gun_control + /Gun_dog + /Gung-ho /Gunpowder /Gunpowder_Incident /Gunpowder_Plot @@ -4204,12 +4876,14 @@ /H_II_region /Habitat /Habitat_destruction + /Hacker /Hacky_sack /Haddock /Hagia_Sophia /Haiku /Hail /Hair + /Hair_dryer /Hairdresser /Hairstyle /Haiti @@ -4217,6 +4891,7 @@ /Halakha /Half-Life_2 /Halibut + /Halifax_Explosion /Hall_effect /Hallmark_Cards /Halloween @@ -4226,6 +4901,7 @@ /Halo_Legends /Halo_Wars /Halogen + /Ham /Hamburger /Hamerkop /Hamlet @@ -4233,19 +4909,25 @@ /Hammerhead_shark /Hamster /Han_dynasty + /Han_Solo + /Hand-me-down + /Hand_tool /Handball + /Handbell /Handcuffs /Handgun /Handheld_game_console - /Handheld_projector - /Handheld_video_game /Handicraft /Handycam + /Hang_gliding + /Hanging /Hank_Aaron /Hanna-Barbera /Hannibal + /Hanoi /Hans_Bethe /Hans_Geiger + /Happening /Happiness /Happy_Birthday_to_You /Happy_Meal @@ -4253,6 +4935,7 @@ /Harbor /Hard_disk_drive /Hard_rock + /Hard_Rock_Cafe /Hardness /Hardwood /Hare @@ -4278,10 +4961,11 @@ /Hasbro /Hash_table /Hat + /Hat-trick /Hatching /Hatmaking /Haumea - /Haunted_Mansion + /Haunted_Mansion_Holiday /Havana /Hawaii /Hawaiian_honeycreeper @@ -4290,6 +4974,8 @@ /Hawk /Hayao_Miyazaki /Hazardous_waste + /Hazel + /Hazelnut /HBO /HD_DVD /HD_Radio @@ -4303,12 +4989,13 @@ /Health_geography /Health_informatics /Health_psychology - /Health_technology /Healthy_diet /Hearing_aid /Hearing_loss /Heart + /Heart_transplantation /Heartbleed + /Hearth /Heat /Heat_capacity /Heat_sink @@ -4321,9 +5008,9 @@ /Hebrew_language /Hebrew_punctuation /Hedgehog - /Hegelianism /Height /Heinrich_Hertz + /Heinz /Heinz_von_Foerster /Hejaz_railway /Helen_Keller @@ -4342,6 +5029,7 @@ /Helsinki /Hematology /Hematopathology + /Hemoglobin /Henotheism /Henri_Becquerel /Henri_Bergson @@ -4354,8 +5042,11 @@ /Herbaceous_plant /Herbicide /Herculaneum + /Herding_dog /Heresy + /Heritage_railway /Hermeneutics + /Hermes /Hermione_Granger /Hermit_crab /Hero @@ -4368,15 +5059,20 @@ /Hesperides /Heuristic /Hewlett-Packard + /Hexadecimal /Hexagonal_chess /Hexane + /Hidden_Mickey /Hide-and-seek /Hierarchy /High-speed_rail /High-yield_debt /High_availability /High_Earth_orbit + /High_fidelity + /High_School_Musical /High_school_radio + /High_Seas_Fleet /High_tech /High_treason /Higher_education @@ -4389,9 +5085,9 @@ /Himalayas /Himeji_Castle /Hindenburg_disaster - /Hindu /Hinduism /Hinny + /Hip_flask /Hip_hop /Hip_hop_fashion /Hip_hop_music @@ -4405,12 +5101,15 @@ /Histology /Histopathology /Historian + /Historic_district /Historic_house + /Historic_preservation /Historic_site /Historical_fantasy /Historical_method /History /History_of_Africa + /History_of_Antarctica /History_of_Asia /History_of_aviation /History_of_biology @@ -4431,6 +5130,8 @@ /History_of_theatre /Hitoshi_Sakimoto /Hives + /HMS_Queen_Mary + /Ho_Chi_Minh_City /Hobby /Hobby_farm /Hockey @@ -4438,11 +5139,15 @@ /Holarctic /Holborn_Viaduct /Holden + /Hole_punch /Holiday /Holism /Holkham_Hall /Hollywood + /Hollywood_Land /Hollywood_Walk_of_Fame + /Holography + /Holy_Grail /Holy_Roman_Emperor /Holy_Roman_Empire /Holy_Spirit @@ -4455,6 +5160,7 @@ /Home_energy_monitor /Home_video /Homemaking + /HomePod /Homer /Homer_Simpson /Homeschooling @@ -4463,6 +5169,7 @@ /Homicide /Homing_pigeon /Hominidae + /Homo_erectus /Homogeneous_space /Homomorphism /Honda @@ -4478,6 +5185,7 @@ /Honeywell /Hong_Kong /Hong_Kong_Disneyland + /Honolulu /Hoodwinked! /Hookworm_infection /Hoop_rolling @@ -4488,6 +5196,7 @@ /Hornbill /Hornet /Hornussen + /Horoscope /Horror_fiction /Horror_film /Horse @@ -4496,15 +5205,21 @@ /Horseshoes /Hospital /Hospodar + /Hostel /Hot_air_balloon /Hot_chocolate /Hot_dog + /Hot_sauce /Hot_tub /Hot_Wheels + /Hotel + /Hotel_California /Hotel_Mario + /Hound /Hours_of_service /House /House_arrest + /Housefly /Household /Houseplant /Houston @@ -4523,6 +5238,7 @@ /HTML /HTTPS /Hua_Mulan + /Huawei /Hubble_Space_Telescope /Hubert_Humphrey /Hudson_River @@ -4531,6 +5247,7 @@ /Hull_House /Hulu /Human + /Human_behavior /Human_biology /Human_body /Human_bonding @@ -4540,10 +5257,15 @@ /Human_capital /Human_condition /Human_ecology + /Human_enhancement /Human_evolution + /Human_eye /Human_geography + /Human_nature + /Human_overpopulation /Human_resources /Human_rights + /Human_sacrifice /Human_science /Human_settlement /Human_spaceflight @@ -4566,6 +5288,7 @@ /Hungry_generation /Hunter-gatherer /Hunting + /Hunting_dog /Hurricane_Anita /Hurricane_Carmen /Hurricane_Eloise @@ -4594,17 +5317,20 @@ /Hymn /Hyperbole /HyperCard + /Hyperion_Theater /Hyperlink /Hyperloop /Hypermedia /Hyperplane /Hypersphere + /Hypertension /Hypertext /Hyperthymesia /Hyphen /Hypnosis /Hypocycloid /Hypotenuse + /Hypothermia /Hypothesis /iAd /Ian_Fleming @@ -4624,6 +5350,7 @@ /Ice_skating /Ice_storm /Iceberg + /Icebreaker /Iced_tea /Iceland /Ichthyology @@ -4641,15 +5368,19 @@ /Identity_element /Ideology /Idris_Elba + /If_You_Had_Wings /Igloo /IGN /Iguana /IKEA /Iliad /Illegal_drug_trade + /Illegal_logging /Illinois /Illinois_Senate + /Illuminated_manuscript /Illuminati + /IllumiNations /Illusion /Illustration /Illustrator @@ -4658,6 +5389,7 @@ /Image_resolution /Imagery /Imagination + /Imagine_Dragons /IMAX /IMAX_Corporation /IMDb @@ -4670,12 +5402,14 @@ /Immunohistochemistry /Immunology /iMovie + /IMovie /Impact_crater /Impact_event /Impact_factor /Impala /Imperialism /Impressionism + /Impressions_de_France /In-N-Out_Burger /In_silico /In_vitro @@ -4685,12 +5419,15 @@ /Inca_mythology /Incandescent_light_bulb /Inception + /Inch /Incineration /Inclined_plane /Income /Incredibles_2 + /Incredicoaster /Independence /Independence_Hall + /Independent_contractor /Independent_film /India /Indian_astronomy @@ -4699,12 +5436,12 @@ /Indian_Railways /Indian_rupee /Indian_rupee_sign + /Indian_War_Canoes /Indiana /Indiana_Jones /Indiana_Jones_Adventure /Indiana_University /Indianapolis_500 - /Indicator_species /Indie_pop /Indie_rock /Indigenous_peoples @@ -4730,6 +5467,7 @@ /Infant /Infection /Inference + /Inferiority_complex /Infill /Infinitesimal /Infinitism @@ -4739,7 +5477,7 @@ /Inflorescence /Influenza /Influenza_vaccine - /Informal_fallacy + /Infographic /Informal_learning /Information /Information_Age @@ -4768,7 +5506,6 @@ /Inquiry /Insane_Clown_Posse /Insect - /Insects_in_culture /Insight /Insomniac_Games /Instagram @@ -4788,33 +5525,37 @@ /Intel /Intel_Compute_Stick /Intellect + /Intellectual /Intellectual_property /Intelligence /Intelligent_agent /Intelligent_Systems /Intelligentsia /Intelsat_I + /Intensive_animal_farming /Intensive_farming /Intentionality /Interaction /Interaction_design - /Interactionism + /Interactive_art /Interactive_fiction /Interactive_media - /Interactive_movie + /Interactivity + /Intercom /Intercropping - /Interdependence /Interdisciplinarity /Interest /Interest_rate /Interfaith_dialogue /Interferometry + /Interior_architecture /Interior_design /Interlaced_video + /Internal_energy /Internal_medicine - /International /International_law /International_relations + /International_Sign /International_trade /International_unit /Internet @@ -4834,8 +5575,10 @@ /Internet_radio /Internet_slang /Internment + /Interpolation /Interpunct /Interrobang + /Interview /Intolerable_Acts /Introgression /Introspection @@ -4854,7 +5597,9 @@ /IOS /Iowa /Iowa_caucuses + /IP_address /IPad + /IPhone /iPhone /iPhone_3G /iPhone_3GS @@ -4868,9 +5613,11 @@ /iPhoto /IPod /IPod_Classic + /IPod_Nano /iPod_Nano /iPod_Shuffle /iPod_Touch + /IPod_Touch /Iran /Iran_hostage_crisis /Iranian_peoples @@ -4888,9 +5635,11 @@ /IRobot /Iron /Iron_Age + /Iron_Curtain /Iron_Man /Iron_Man_2 /Iron_Man_3 + /Iron_Man_Experience /Iron_ore /Ironclad_warship /Ironing @@ -4905,6 +5654,7 @@ /Isidore_of_Seville /Isis /Islam + /Islamabad /Islamic_art /Islamic_calendar /Islamic_culture @@ -4914,8 +5664,8 @@ /Islamism /Islamophobia /Island - /Islands_of_Adventure /Isle_of_Wight + /Isometric_projection /Isometry /Isomorphism /Isotope @@ -4924,9 +5674,12 @@ /Israeli_settlement /Israelites /Istanbul + /It%27s_a_Small_World + /Italian_beef /Italian_Fascism /Italian_literature /Italian_neorealism + /Italian_opera /Italian_Renaissance /Italian_Wars /Italians @@ -4942,7 +5695,9 @@ /J._K._Rowling /J._R._R._Tolkien /Jabba_the_Hutt - /Jacana + /Jack-in-the-box + /Jack-o%27-lantern + /Jack_in_the_Box /Jack_Sparrow /Jack_the_Ripper /Jackal @@ -4951,13 +5706,16 @@ /Jaguar /Jainism /Jake_Gyllenhaal + /Jalape%C3%B1o /Jamaica /Jamba_Juice /James_Bond /James_Cameron + /James_Comey /Jamie_Foxx /January /Japan + /Japan_Pavilion_at_Epcot /Japanese_art /Japanese_language /Japanese_mythology @@ -4984,10 +5742,12 @@ /Jazz_violin /Jealousy /Jedi + /Jedi_Training_Academy /Jeep /Jeff_Bezos /Jellyfish /Jenga + /Jeopardy! /Jerome /Jersey /Jerusalem @@ -4999,6 +5759,7 @@ /Jet_bridge /Jet_engine /Jet_Force_Gemini + /Jet_plane /Jet_stream /JetBlue /Jetpac @@ -5012,18 +5773,22 @@ /Jim_Henson /Jimmy_Carter /Jimmy_Fallon + /Jimmy_John%27s /Johannes_Hevelius /Johannesburg /John_Cena /John_D._Rockefeller /John_Dalton + /John_Deere /John_F._Kennedy + /John_Hancock_Center /John_Heisman /John_Lasseter /John_McCain /John_Ratzenberger /John_Williams /Johnny_Depp + /Johnny_Rockets /Joiner /Joint_venture /Joke @@ -5031,12 +5796,13 @@ /Jon_Stewart /Jonagold /Jonah_Hill - /Jonathan_Ive /Jordan /Josef_Mengele /Joseph_Gordon-Levitt /Joseph_Stalin + /Josh_Gad /Joss_Whedon + /Joule /Journalism /Journalist /Jousting @@ -5046,14 +5812,19 @@ /JPMorgan_Chase /JSON /Judaism + /Judge_Judy /Judgement /Judiciary + /Judo /Juggling + /Juice /Jukebox /Julia_Roberts /Julian_Assange + /Juliet /July /Jumbotron + /Jumpin%27_Jellyfish /Jumping /Jumpsuit /June @@ -5061,6 +5832,7 @@ /Jungle_cat /Jungle_Cruise /Junk_food + /Junk_science /Jupiter /Jurassic /Jurassic_Park @@ -5077,7 +5849,10 @@ /Justin_Timberlake /Justin_Trudeau /K-pop + /Kaaba + /Kabul /Kaleidoscope + /Kali_River_Rapids /Kameo /Kangaroo /Kangaroo_rat @@ -5105,7 +5880,11 @@ /Kazakhstan /Kazakhstani_tenge /KDE_Software_Compilation + /Kebab + /Keg + /Kelp /Kelvin + /Kenan_Thompson /Kendrick_Lamar /Kenji_Ito /Kennedy_Space_Center @@ -5119,13 +5898,17 @@ /Kevin_Spacey /Kevin_Warwick /Keystone_Pipeline + /KFC /Khan_Noonien_Singh /Kia_Motors /Kickball /Kickboxing /Kickstarter + /Kidney /Kidney_bean + /Kidney_transplantation /Kiev + /Kilimanjaro_Safaris /Killer_whale /Kilogram /Kilometre @@ -5139,16 +5922,22 @@ /Kinetic_energy /Kinetoscope /King_Arthur + /King_Arthur_Carrousel + /King_cake /Kingdom_Hearts + /Kingdom_of_Great_Britain /Kingfisher /Kinglet /Kings_Row /Kingsley_Amis /Kinship /Kirsten_Dunst + /Kit_Kat /Kitchen + /Kitchen_Kabaret /Kite /Kiwi + /Kiwifruit /Klaus_Fuchs /Knife /Knife_throwing @@ -5167,40 +5956,49 @@ /Komodo_dragon /Konqueror /Korea + /Korean_animation /Korean_barbecue /Korean_language /Korean_mythology /Korean_Peninsula /Korean_punctuation /Korean_War + /Kowloon /Kraft_Foods + /Kraft_Heinz /Kreia /Kriging /Krill + /Krispy_Kreme /Krypton /Kuala_Lumpur /Kuiper_belt /Kung_Fu_Panda_3 /Kuwait /Kyocera + /Kyoto /Kyoto_Protocol /Kyrgyzstan /LA_Galaxy /La_Paz /La_Vega_Province /Label + /Labor_Day /Laboratory /Laboratory_flask /Laboratory_rat /Labour_economics /Labour_law + /Labrador_Retriever /LabVIEW /Lacrosse + /Lactic_acid /Ladakh /Ladin_language /Lady_and_the_Tramp /Lady_Gaga /Lagoon + /Lagos /Lake /Lake_Baikal /Lake_Chad @@ -5216,17 +6014,21 @@ /Lance_Armstrong /Land-use_planning /Land_art + /Land_degradation /Land_law /Land_mine /Land_patent /Land_snail + /Land_transport /Land_trust /Land_use /Landfall /Landfill /Landform + /Landing_craft_tank /Landlord /Landscape + /Landscape_architect /Landscape_architecture /Landscape_design /Landscape_ecology @@ -5239,14 +6041,18 @@ /Lantana /Lantana_camara /Lao_kip + /Lapel_pin /Laptop /Lara_Croft /Large_Hadron_Collider /Laridae /Lark /Larry_Ellison + /Larry_the_Cable_Guy /Larynx /Las_Vegas + /Las_Vegas_Strip + /Las_Vegas_Valley /Lascaux /Laser /Laser_cutting @@ -5272,18 +6078,22 @@ /Lava_lamp /Law /Law_and_economics + /Law_enforcement /Lawn /Lawn_mower /Lawrence_Lessig /Lawyer /Lazy_river + /Le_Carrousel_de_Lancelot /Le_Corbusier /Lead + /Lead_glass /Lead_poisoning /Leadership /Leaf /Leafcutter_ant /League_of_Legends + /League_of_Nations /Lean_manufacturing /Leap_year /Leapfrog @@ -5300,7 +6110,9 @@ /LeBron_James /Lectin /LED_display + /LED_lamp /Led_Zeppelin + /Led_Zeppelin_IV /Lee_Unkrich /Leech /Legal_drama @@ -5321,6 +6133,7 @@ /Lehman_Brothers /Leicester_City_F.C. /Leisure + /Lemon /Lemonade /Lemur /Lena_Headey @@ -5333,13 +6146,17 @@ /Leonhard_Euler /Leonidas_I /Leopard + /Les_Mis%C3%A9rables /Lesbian + /Let_It_Be /Lettuce /Lev_Vygotsky /Levee /Level_design /Lever + /Leveraged_buyout /Lewis_acids_and_bases + /Lewis_Black /Lexicology /Lexicon /Lexmark @@ -5350,16 +6167,23 @@ /Liberia /Libertarianism /Liberty + /Liberty_Belle_Riverboat + /Liberty_ship /Librarian /Library /Library_Journal + /Library_of_Alexandria + /Library_of_Congress /Library_science + /Libya /License /License_manager /Lichen + /Lid /Lie /Lie_algebra /Lie_group + /Lieutenant_general /Life /Life_expectancy /Life_extension @@ -5373,13 +6197,17 @@ /Light-emitting_diode /Light-year /Light_gun_shooter + /Light_Magic /Light_pollution /Light_rail + /Lighter /Lighthouse /Lighting /Lightning + /Lightning_McQueen /Lightning_rod /Lightsaber + /Lilo_%26_Stitch /Limestone /Limnology /Limpet @@ -5390,12 +6218,14 @@ /Line_segment /Linear_algebra /Linear_equation + /Linear_motor /Linear_programming /Linear_regression /Linguistics /Link_layer /Linked_list /LinkedIn + /Linkin_Park /Linnaean_taxonomy /Linux /Linux_kernel @@ -5405,33 +6235,43 @@ /Lionel_Messi /Lionsgate /Lipid + /Liqueur /Liquid /Liquid-crystal_display /Liquid_crystal + /Liquid_nitrogen /Lisbon /Lisp_machine /Lisztomania /Lite-Brite /Literacy + /Literal_translation /Literary_criticism /Literary_fiction /Literary_nonsense /Literary_theory /Literature /Lithium + /Lithium-ion_battery /Lithuania /Litre + /Little_Bo-Peep /Little_Busters! + /Little_green_men /LittleBigPlanet + /Littoral_combat_ship /Liturgy /Liu_Kang /Live_action /Live_steam /Liver + /Liver_transplantation /Liverpool /Liverpool_F.C. /Livestock + /Livin%27_on_a_Prayer /Living_room + /Living_with_the_Land /Lizard /Llama /Load_management @@ -5446,6 +6286,7 @@ /Locomotive /Locus_of_control /Locust + /Lodging /Logarithm /Logging /Logic @@ -5458,12 +6299,12 @@ /Logical_consequence /Logical_form /Logical_positivism - /Logical_reasoning /Logical_truth /Logistics /Logo /Logogram /Logos + /Lollipop /London /London_Eye /London_sewerage_system @@ -5473,6 +6314,7 @@ /Long-term_memory /Long_hair /Long_Island + /Long_John_Silver /Long_jump /Longbow /Longevity @@ -5485,6 +6327,7 @@ /Lorne_Michaels /Los_Angeles /Los_Angeles_Times + /Lose_Yourself /Lost_film /Lotfi_A._Zadeh /Lottery @@ -5499,7 +6342,9 @@ /Louvre /Love /Love_Hina + /Low-cost_carrier /Low_culture + /Low_Earth_orbit /Lower_Saxony /Lozenge /LucasArts @@ -5509,8 +6354,10 @@ /Lufthansa /Luftwaffe /Luigi + /Luigi%27s_Flying_Tires /Luigi_Federico_Menabrea /Luke_Cage + /Luke_Skywalker /Lumber /Luminous_Studio /Luna_programme @@ -5518,7 +6365,6 @@ /Lunar_water /Lunch /Lunch_meat - /Lunchbox /Lung /Lungfish /Lurene_Tuttle @@ -5527,14 +6373,16 @@ /Lux_Radio_Theatre /Luxembourg /Luxo_Jr. + /Luxor_Las_Vegas + /Luxury_goods /Lyceum + /Lyft /Lyme_Regis /Lynx /Lyon /Lyrebird /M-learning /MAC_address - /Mac_App_Store /Mac_Mini /Mac_OS_X_Lion /Mac_OS_X_Panther @@ -5548,10 +6396,13 @@ /MacBook_Pro /Machete /Machine + /Machine_code /Machine_gun /Machine_learning /Machine_press + /Machine_tool /Machinima + /Machining /Machu_Picchu /Macintosh /Macintosh_128K @@ -5567,19 +6418,24 @@ /Macy%27s /Mad_Libs /Mad_Men + /Mad_T_Party + /Mad_Tea_Party /Madagascar /Madrid /Mafia_film /Magazine /Maggie_Gyllenhaal /Magic_8-Ball + /Magic_Carpets_of_Aladdin /Magic_in_fiction /Magic_Johnson + /Magic_Journeys /Magic_Kingdom /Magic_lantern /Magic_Mouse /Magic_realism /Magic_Trackpad + /Maglev /Magna_Carta /Magnavox_Odyssey /Magnesium @@ -5593,7 +6449,6 @@ /Mahjong /Mail /Mail_order - /Main_Page /Main_sequence /Main_Street /Maine @@ -5609,10 +6464,10 @@ /Major_League_Gaming /Major_League_Soccer /Major_religious_groups - /Majorca + /Make-A-Wish_Foundation /Make-up_artist + /Make_America_Great_Again /Makemake - /Maker_Studios /Making_a_Murderer /Malacology /Malala_Yousafzai @@ -5621,7 +6476,9 @@ /Malcolm_X /Mali /Mali_Empire + /Maliboomer /Mallard + /Mallet /Malling_series /Malmesbury /Malnutrition @@ -5637,16 +6494,23 @@ /Management /Manakin /Manatee + /Manchester_United_F.C. /Mandrill /Manga /Mango + /Mango_float /Manhattan + /Manhattan_Project /Maniac_Mansion + /Manifest_destiny /Manifesto /Manifold + /Manila /Manila_Hotel /Mannerism /Mannheim + /Manor_house + /Mansion /Manslaughter /Manta_ray /Mantis @@ -5657,9 +6521,12 @@ /Map /Maple_bacon_donut /Maple_syrup + /Mar-a-Lago /Marathi_language /Marathon + /Marble /Marble_Madness + /Marbury_v._Madison /Marcel_Duchamp /March /March_on_Rome @@ -5668,11 +6535,17 @@ /Margaret_Boden /Margaret_Fuller /Margaret_Thatcher + /Margarine /Mariah_Carey /Marian_Rejewski /Mariana_Trench /Marie_Curie + /Marina + /Marina_Bay_Sands + /Marination /Marine_biology + /Marine_conservation + /Marine_debris /Marine_engineering /Marine_geology /Marine_life @@ -5683,23 +6556,28 @@ /Marines /Mario /Mario_Bros. + /Mario_Kart /Mario_Kart_64 /Mario_Kart_Wii + /Mario_Party /Maritime_museum /Maritime_transport /Mark_Hamill /Mark_McGwire /Mark_Ruffalo /Mark_Twain + /Mark_Twain_Riverboat /Mark_Wahlberg /Mark_Zuckerberg /Marker_pen /Market_capitalization + /Market_liquidity /Marketing /Marlin /Marmolada /Marmoset /Marmot + /Maroon_5 /Marquesas_Islands /Marquetry /Marriage @@ -5713,6 +6591,7 @@ /Marshall_Islands /Marshall_Plan /Marshalsea + /Marshmallow /Marsupial /Marten /Martha_Stewart @@ -5734,15 +6613,16 @@ /Marxist_sociology /Mary_Anning /Maryland + /Marzipan /Masashi_Hamauzu /Mascot /Mashable + /Mashed_potato /Mask /Masonry /Mass /Mass_communication /Mass_Effect - /Mass_games /Mass_media /Mass_production /Mass_society @@ -5750,11 +6630,14 @@ /Mass_surveillance /Mass_transfer /Massachusetts + /Massachusetts_Bay /Massif /Master_of_Orion + /Master_of_Science /Master_System /MasterCard /Mastodon + /Material_culture /Materialism /Materials_science /Mathematical_analysis @@ -5766,6 +6649,7 @@ /Mathematics /Mating /MATLAB + /Matryoshka_doll /Mattel /Matter /Matterhorn @@ -5786,18 +6670,24 @@ /Mayan_languages /Mayflower /Mayflower_Compact + /Mayonnaise + /Mayor /Mazda /Maze /McCarthyism /McDonald%27s /McDonnell_Douglas + /McKinsey_%26_Company + /Me_Too_movement /Meal + /Mean_Girls /Meaning_of_life /Measurement /Meat /Meat_cutter /Meat_extract /Meat_packing_industry + /Meatball /Mecca /Mecha /Mechanical_engineering @@ -5805,6 +6695,7 @@ /Mechanics /Mechanosynthesis /Mechatronics + /Medell%C3%ADn_Cartel /Media_psychology /MediaWiki /Medicaid @@ -5821,6 +6712,7 @@ /Medicine /Medicine_man /Medieval_art + /Meditation /Mediterranean_Sea /Meerkat /Meg_Whitman @@ -5851,6 +6743,7 @@ /Memory_inhibition /Memory_Stick /Memristor + /Men_in_Black_II /Mena_Suvari /Menacer /Mental_disorder @@ -5863,10 +6756,12 @@ /Mercantilism /Mercator_projection /Mercedes-Benz + /Mercenary /Merchandising /Merchant /Merchant_vessel /Mercury_poisoning + /Merlin /Mermaid /Merriam-Webster /Merrill_Lynch @@ -5904,13 +6799,17 @@ /Metro_station /Metroid /Metroid_Prime + /Metropolitan_area /Mewtwo /Mexican_art + /Mexican_Cession /Mexican_Drug_War /Mexican_muralism /Mexican_Revolution /Mexico /Mexico_City + /Mexico_Pavilion_at_Epcot + /Meze /Mia_Hamm /Miami /Miami_Dolphins @@ -5922,40 +6821,56 @@ /Michael_Jackson /Michael_Jordan /Michelangelo - /Michelin_Man + /Michelin /Michelle_Obama /Michigan + /Mickey%27s_Fun_Wheel + /Mickey%27s_House + /Mickey%27s_PhilharMagic + /Mickey%27s_Toontown /Mickey_Mantle /Mickey_Mouse /Mickey_Mouse_Clubhouse + /Mickey_Mouse_Revue + /Microarchitecture /Microbial_ecology + /Microbiologist /Microbiology /Microbotics /Microburst + /Microcontroller /Microeconomics /Microelectronics /Microfabrication /Microgram /Micromanagement + /Micrometre /Microorganism /Micropaleontology /Microphone + /Microprocessor /Microscope /Microsoft /Microsoft_Edge /Microsoft_PowerPoint /Microsoft_Windows + /Microsoft_Word /Microtransaction /Microwave /Microwave_oven /Mid-century_modern /Middle-earth /Middle_Ages + /Middle_class /Middle_East + /Midget_Autopia /MIDI /Midtown_Manhattan /Midwestern_United_States /Midwifery + /Migraine + /Mike_Fink_Keel_Boats + /Mike_Wazowski /Mildew /Mileena /Milgram_experiment @@ -5965,6 +6880,8 @@ /Military_aircraft /Military_alliance /Military_aviation + /Military_base + /Military_Cross /Military_engineering /Military_exercise /Military_history @@ -5986,7 +6903,10 @@ /Millennialism /Millennials /Millennium + /Millennium_Dome + /Millennium_Falcon /Millennium_Stadium + /Millennium_Village /Millipede /Millwright /Milton_Friedman @@ -5994,7 +6914,6 @@ /Milwaukee_Brewers /Milwaukee_River /Mime_artist - /Mimid /Minamata_disease /Mind /Mind_map @@ -6005,12 +6924,13 @@ /Mindy_Kaling /Minecraft /Mineral + /Mineral_rights /Mineralogy /Ming_dynasty /Mini /Mini_Moke - /Miniclip /Minicomputer + /Minigame /Minimalism /Minimax /Minimum_wage @@ -6023,11 +6943,15 @@ /Minneapolis /Minnesota /Minnesota_Twins + /Minnie%27s_House /Minnie_Mouse /Minnow /Minoan_pottery /Minor-planet_moon /Minor_League_Baseball + /Mint_condition + /Mir + /Miracle_Whip /Miramax /Miranda_v._Arizona /Mirror @@ -6036,12 +6960,15 @@ /Miss_Piggy /Missile /Missing_in_action + /Missing_person + /Mission_to_the_Moon /Mississippi /Mississippi_River /Missouri /Missouri_Compromise /Missouri_River /Missouri_Territory + /MIT_Press /Mite /Mitochondrion /Mitosis @@ -6050,14 +6977,17 @@ /Mixed_drink /Mixed_martial_arts /Mixed_media + /Mixtape /Mixture /MLB_Advanced_Media /Mnemonic + /Moai /Mobile_app /Mobile_blogging /Mobile_computing /Mobile_device /Mobile_game + /Mobile_operating_system /Mobile_payment /Mobile_phone /Mobile_robot @@ -6066,7 +6996,6 @@ /Mockingbird /Modal_logic /Model_aircraft - /Model_building /Model_car /Model_theory /Modem @@ -6083,12 +7012,15 @@ /Modernity /Modular_arithmetic /Modulation + /Modus_operandi /Moe_Berg /Mohawk_hairstyle /Moisture /Mojang /Mojave_Desert + /Mojave_National_Preserve /Molar_concentration + /Molasses /Moldavia /Molecular_biology /Molecular_cloud @@ -6102,12 +7034,16 @@ /Mollusca /Mom_and_Dad /Mona_Lisa + /Monaco /Monarch /Monarchy /Monday /Monetary_economics + /Monetary_policy /Money + /Money_creation /Money_laundering + /Money_supply /Mongol_Empire /Mongolia /Mongols @@ -6128,16 +7064,19 @@ /Monotheism /Monroe_Doctrine /Monsoon + /Monster_Energy /Monster_movie /Monsters_University /Montana /Monte_Carlo /Montesquieu /Montgolfier_brothers + /Montreal /Monty_Python /Mood_disorder /Moon /Moon_landing + /Mooncake /Moons_of_Haumea /Moons_of_Jupiter /Moons_of_Mars @@ -6152,6 +7091,7 @@ /Moral_character /Morale /Morality + /Mormon_Trail /Mormons /Morning_glory /Morocco @@ -6178,10 +7118,14 @@ /Motility /Motion_blur /Motion_capture + /Motion_comic /Motion_controller /Motion_detection + /Motion_sickness /Motion_simulator /Motivation + /Motor_Boat_Cruise + /Motor_control /Motor_controller /Motor_coordination /Motor_skill @@ -6194,10 +7138,14 @@ /Motorsport /Mount_Everest /Mount_Fuji + /Mount_Kilimanjaro + /Mount_of_the_Holy_Cross /Mount_Olympus /Mount_Rushmore /Mount_St._Helens + /Mount_Tai /Mount_Tambora + /Mount_Vesuvius /Mountain /Mountain_bike /Mountain_goat @@ -6205,22 +7153,30 @@ /Mountaineering /Mouse /Mousebird + /Movie_camera /Movie_projector /Movie_star /Movie_theater /Moviefone /Moving_walkway /Mowgli + /Mozilla + /Mozzarella /MP3_player /Mr._Bean /Mr._Potato_Head + /Mr._Toad%27s_Wild_Ride + /Ms._Pac-Man /MSN /MSNBC /MTR /MTV + /Mucus + /Muffin /Muggle /Muhammad_Ali /Mule + /Mulholland_Madness /Multi-licensing /Multi-touch /Multicellular_organism @@ -6232,18 +7188,21 @@ /Multiple_unit /Multiplication /Mumbai + /Mummy /Munich /Municipal_solid_waste /Municipality /Muppet*Vision_3D /Mural - /Murdeshwar + /Murder%E2%80%93suicide /Muscle /Muscle_car /Muses /Museum + /Museum_of_Modern_Art /Mushroom /Music + /Music_box /Music_education /Music_festival /Music_genre @@ -6255,7 +7214,9 @@ /Music_technology /Music_theory /Music_therapy + /Music_venue /Music_video + /Music_video_game /Musical_chairs /Musical_composition /Musical_ensemble @@ -6268,8 +7229,12 @@ /Musket /Muskox /Muslim + /Muslim_conquest_of_Egypt + /Muslim_world /Mustang /Mutation + /Mute_swan + /Mycelium /Mycology /Myles_Standish /Myr @@ -6279,12 +7244,18 @@ /Myst /Mystery_House /Mysticism + /Myth + /MythBusters /Mythology /N.W.A + /Na%27vi_River_Journey + /Nachos + /Nagasaki /Naive_set_theory /Naked_mole-rat /Namco /Name + /Nancy_Drew /Nancy_Pelosi /Nancy_Reagan /Nanoelectronics @@ -6298,15 +7269,17 @@ /Nanostructure /Nanotechnology /Nanowire + /Nantucket /Napalm /Napoleon /Napoleonic_era /Napoleonic_Wars /Napster /Narcissism + /Narcos /Narrative - /Narrative_hook /Narrative_poetry + /Narrow-gauge_railway /Narrowband /Naruto /Narwhal @@ -6316,9 +7289,16 @@ /Nation_state /National_anthem /National_dish + /National_Fascist_Party + /National_Gallery + /National_Gallery_of_Art + /National_Geographic /National_Hockey_League /National_identity + /National_Mall + /National_Palace_Museum /National_park + /National_Park_Service /National_Security_Agency /National_sport /Nationalism @@ -6329,6 +7309,7 @@ /Natural_environment /Natural_fiber /Natural_gas + /Natural_hazard /Natural_history /Natural_language /Natural_law @@ -6342,10 +7323,14 @@ /Naturalization /Nature /Nature_reserve + /Nature_versus_nurture + /Navajo_National_Monument /Naval_architecture + /Naval_mine /Naval_ship /Navigation /Navy + /Navy_Pier /Nazi_Germany /Nazi_Party /Nazism @@ -6361,6 +7346,7 @@ /Nebular_hypothesis /NEC /Needlefish + /Needlework /Negativity_bias /Nehalennia /Neighbourhood @@ -6427,6 +7413,7 @@ /Neutron_capture /Neutron_star /Nevada + /Nevada_Test_Site /Nevado_del_Ruiz /New-age_music /New_Age @@ -6441,12 +7428,15 @@ /New_Mexico /New_moon /New_Orleans + /New_Orleans_Square /New_START /New_Statesman /New_Super_Mario_Bros. + /New_Testament /New_Urbanism /New_World /New_Year + /New_Year%27s_Day /New_Year%27s_Eve /New_York-style_pizza /New_York_City @@ -6459,12 +7449,14 @@ /New_York_Yankees /New_Zealand /New_Zealand_wine + /Newgate_Prison /News /News_broadcasting /News_Corp /News_Corporation /News_media /Newseum + /Newsletter /Newspaper /Newsprint /NewsRadio @@ -6482,6 +7474,7 @@ /Nick_Fury /Nickel /Nickelodeon + /Nicotine /Niels_Bohr /Nielsen_ratings /Niger @@ -6491,6 +7484,7 @@ /Night_at_the_Museum /Night_sky /Night_Trap + /Night_vision /Nightclub /Nightjar /Nights_into_Dreams @@ -6501,6 +7495,7 @@ /Nile_crocodile /Nimrud /Nine_Inch_Nails + /Nineteen_Eighty-Four /Nintendo /Nintendo_2DS /Nintendo_3DS @@ -6512,6 +7507,7 @@ /Nitric_acid /Nitrogen /No_Russian + /Noah%27s_Ark /Noam_Chomsky /Nobel_Peace_Prize /Nobel_Prize @@ -6520,10 +7516,14 @@ /Nobuo_Uematsu /Noise_pollution /Nokia + /Non-fiction /Nondualism /Nonlinear_system + /Nonprofit_organization /Nontheism + /Nonverbal_communication /Noodle + /Noodles_%26_Company /Nootropic /Norbert_Wiener /Nordic_skiing @@ -6536,6 +7536,7 @@ /Normative_ethics /Norris_Bradbury /Norse_mythology + /Norsemen /North_Africa /North_America /North_Asia @@ -6543,7 +7544,9 @@ /North_Dakota /North_Korea /North_Sea + /North_Vietnam /Northern_blot + /Northern_Europe /Northern_Hemisphere /Northern_Ireland /Northern_Spy @@ -6554,32 +7557,41 @@ /Norwegian_Americans /Norwegian_language /Norwich_University + /Nostalgia /Not_One_Less + /Notre-Dame_de_Paris /Noumenon /Novak_Djokovic /Novel /Novelist + /Novelization /November /November_Uprising /NPR /Nth_root /NTSC /Nuclear_arms_race + /Nuclear_chain_reaction /Nuclear_chemistry /Nuclear_disarmament /Nuclear_engineering + /Nuclear_fallout /Nuclear_family /Nuclear_fission /Nuclear_fuel /Nuclear_fusion + /Nuclear_medicine /Nuclear_meltdown /Nuclear_physics /Nuclear_power + /Nuclear_power_plant + /Nuclear_proliferation /Nuclear_reaction /Nuclear_reactor /Nuclear_technology /Nuclear_warfare /Nuclear_weapon + /Nuclear_winter /Nudity /Numbat /Number @@ -6588,12 +7600,15 @@ /Number_theory /Numerical_analysis /Numero_sign + /Numismatics /Nun /Nuremberg /Nuremberg_Laws /Nuremberg_trials /Nursing + /Nutcracker /Nuthatch + /Nutmeg /Nutrient /Nutrition /Nvidia @@ -6604,20 +7619,26 @@ /Oak /Oakland_Athletics /Oakland_Raiders + /Oasis /Oat /Obelus /Obesity /Object_database /Object_permanence + /Objective-C + /Obscenity /Observation /Observation_tower + /Observational_study /Obstetrics + /Obstruction_of_justice /OCaml /Occult /Occultation /Occupy_movement /Occupy_Wall_Street /Ocean + /Ocean_acidification /Ocean_chemistry /Ocean_liner /Ocean_planet @@ -6630,6 +7651,7 @@ /Octopus /Oculus_Rift /Odin_Sphere + /Odor /Odwalla /Odysseus /Odyssey @@ -6642,8 +7664,10 @@ /Ohio /Ohio_River /Ohio_State_University + /Ohm%27s_law /Oil /Oil_field + /Oil_paint /Oil_painting /Oil_refinery /Oil_reserves @@ -6658,12 +7682,16 @@ /Old_Louisville /Old_Norse_religion /Old_World + /Old_World_monkey /Olfaction /Olive + /Olive_oil /Oliver_Sacks + /Olmecs /Olympic_flame /Olympic_Games /Olympic_medal + /Olympic_National_Park /Olympic_sports /Olympic_Stadium /Olympic_Village @@ -6673,6 +7701,8 @@ /Oncology /One_Bayfront_Plaza /One_Times_Square + /One_World_Trade_Center + /Onion /Onion_dome /Online_advertising /Online_banking @@ -6687,8 +7717,12 @@ /Oology /Oort_cloud /OPEC + /Open-pit_mining + /Open-source_license /Open-source_model + /Open-source_software /Open_society + /Opening_credits /OpenVMS /Opera /Operating_system @@ -6707,27 +7741,34 @@ /Optical_engineering /Optical_fiber /Optical_illusion - /Optical_phenomena /Optics /Optimism /Optoelectronics /Optometry /Oracle_Corporation /Oracle_Database + /Oral_history /Oral_hygiene + /Oral_tradition /Orality /Orange_Is_the_New_Black + /Orange_Stinger /Orangutan /Orbit /Orbit_of_the_Moon /Orbital_mechanics /Orchard /Orchestra + /Order_of_succession /Order_theory /Ordinal_indicator + /Ore /Oregon /Oregon_Country /Oregon_Trail + /Oreo + /Organ_donation + /Organ_transplantation /Organic_chemistry /Organic_compound /Organic_farming @@ -6740,7 +7781,9 @@ /Organizational_culture /Organized_crime /Oribi + /Origin_of_birds /Origin_story + /Original_sin /Orion_Nebula /Orkney /Ormolu @@ -6757,12 +7800,15 @@ /OS_X_Mavericks /OS_X_Mountain_Lion /OS_X_Yosemite + /Osaka /Osama_bin_Laden /Osamu_Tezuka /Oscar_Isaac /Oseberg_Ship /OSI_model /Oslo + /Ostrich + /Oswald_the_Lucky_Rabbit /Otaku /Othello /Otorhinolaryngology @@ -6770,13 +7816,18 @@ /Otto_Lilienthal /Ottoman_Empire /Oud + /Ouija /Our_Common_Future /Outdoor_recreation /Outer_space /Outlaw_country /Outsourcing + /Ouya /Oven /OverClocked_ReMix + /Overconsumption + /Overexploitation + /Overfishing /Overpopulation /Owl /Ox @@ -6806,14 +7857,16 @@ /Pager /Pain /Paint + /Paint_It_Black + /Paint_the_Night /Paintball - /Painted-snipe /Painting /Pajamas /Pakistan /Pakistan_Railways /Pakistan_studies /PAL + /Palace_of_Versailles /Palaeognathae /Palau /Palazzo_Pitti @@ -6834,10 +7887,13 @@ /Panasonic /Panavision /Pancake + /Pancreas + /Panda_Express /Pandeism /Pandemic /Pandora_Radio /Panentheism + /Panera_Bread /Pangaea /Pangolin /Panic!_at_the_Disco @@ -6848,15 +7904,19 @@ /Panthera_hybrid /Pantothenic_acid /Pantry + /Panzerotti + /Papa_John%27s_Pizza /Paper /Paper_size /Paperback + /Papermaking /Paperman /Papua_New_Guinea /Papyrus /Parabolic_antenna /Parachute /Parade + /Paradigm_shift /Paradox /Paraguay /Parakeet @@ -6864,6 +7924,8 @@ /Parallel_computing /Paralympic_Games /Paramagnetism + /Paramount_leader + /Paramount_Network /Paramount_Pictures /Paranormal /Parapsychology @@ -6871,15 +7933,16 @@ /Parasitology /Paratrooper /Parchment - /Pardalote /Parent /Parent_company /Pareto_efficiency /Paris /Paris_Agreement /Paris_Peace_Accords + /Paris_Saint-Germain_F.C. /Park /Parking + /Parkinson%27s_disease /Parks_and_Recreation /Parlement /Parliament @@ -6888,20 +7951,25 @@ /Parody_film /Parrot /Parrotfish + /Parsley /Parthenon /Particle /Particle_physics /Partly_Cloudy + /Partnership /Partridge /Party /Party_game + /Partysaurus_Rex /Passenger /Passerine /Passover /Passport + /Password /Past /Pasta /Pastel + /Pastiche /Pastoralism /Pastry /Pastry_chef @@ -6912,6 +7980,7 @@ /Pather_Panchali /Pathology /Pathos + /Patient /Patrick_Stewart /Patriot_Act /Patronage @@ -6929,25 +7998,35 @@ /PC_Magazine /PCC_streetcar /PDF + /Pea + /Pea_pod /Peace /Peace_Corps /Peace_education /Peace_movement /Peace_treaty /Peacekeeping + /Peach /Peafowl /Peak_oil /Peano_axioms + /Peanut + /Peanut_butter /Peanuts /Pear + /Pearl /Pearl_Harbor /Pearl_Jam /Pearson_Education + /Peasant + /Peasants%27_Revolt + /Pecan /Pectin /Pedagogy /Pedestrian /Pediatrics /Peer-to-peer + /Peer_pressure /Peer_review /Peering /Pegasus @@ -6964,17 +8043,22 @@ /Pencil /Pendle_Hill /Pendle_witches - /Penduline_tit /Pendulum /Penguin /Penguin_Books + /Penguins_of_Madagascar /Penicillin /Peninsula + /Peninsular_War /Pennsylvania + /Penny /Penny_Pritzker /Penstock /People + /People%27s_Bank_of_China /People_mover + /PeopleMover + /Peppermint /Pepsi /Per_mille /Percent_sign @@ -6985,18 +8069,24 @@ /Peregrine_falcon /Perennial_plant /Perfect_Dark + /Perfect_game /Performance /Performance_art /Performance_poetry /Performing_arts /Perfume + /Pergamon /Periodic_table /Periodical_literature + /Periodization /Periodontology + /Peripatetic_school /Peripheral /Peripheral_vision /Perl + /Permanent_residency /Permutation + /Perry_the_Platypus /Persian_Empire /Persian_Gulf /Persian_language @@ -7009,10 +8099,13 @@ /Personality /Personality_test /Personality_type + /Personalized_medicine + /Persuasion /Peru /Pescasseroli /Pessimism /Pesticide + /Pesto /Pet /Pet_Sounds /Petal @@ -7020,18 +8113,22 @@ /Peter_Griffin /Peter_Jennings /Peter_Pan + /Peter_Pan%27s_Flight /Petra /Petroleum /Petroleum_engineering /Petroleum_industry /Petroleum_politics /Petrology + /Petronas_Towers /PewDiePie /Peyton_Manning /Pez + /Pez_dispenser /Pfennig /PG_Tips /Phacochoerus + /Phaleristics /Phantom_Manor /Pharaoh /Pharmaceutical_drug @@ -7053,6 +8150,7 @@ /Phil_Schiller /Philadelphia /Philadelphia_Eagles + /Philately /Philip_Miller /Philip_Seymour_Hoffman /Philippa_Foot @@ -7061,6 +8159,8 @@ /Philips /Philology /Philosopher + /Philosopher%27s_stone + /Philosophical_analysis /Philosophy /Philosophy_of_film /Philosophy_of_law @@ -7076,6 +8176,7 @@ /Phoenicopteridae /Phonetics /Phonograph + /Phonograph_record /Phonology /Phosphorus /Photo_comics @@ -7120,6 +8221,9 @@ /Pianist /Piano /Piciformes + /Pickled_cucumber + /Pickled_fruit + /Pickling /Pickup_truck /Picnic /Pictionary @@ -7128,6 +8232,7 @@ /Pier /Pierre_Duhem /Pig + /Piggy_bank /Pigment /Pigpen_cipher /Pikachu @@ -7135,8 +8240,11 @@ /Pilgrim /Pilgrimage /Pillow + /Pillsbury_Company /Pilot_whale /Pinball + /Pine + /Pineapple /Pinewood_Studios /Pink /Pinniped @@ -7148,6 +8256,10 @@ /Pipeline_transport /Piracy /Piranha + /Pirates_of_the_Caribbean + /Pistachio + /Pit_bull + /Pita /Pitcher /Pitta /Pittsburgh @@ -7155,21 +8267,27 @@ /Pixar_Canada /Pixel /Pixel_art + /Pixie_Hollow /Pixies /Pizza + /Pizza_delivery /Pizza_Hut /Placebo + /Placenta /Plain + /Plain_text /Plan /Planck_constant /Planck_length /Planet /Planet_Hollywood /Planetarium + /Planetary_boundaries /Planetary_core /Planetary_science /Planktology /Plankton + /Planned_maintenance /Planning /Plant /Plant_cell @@ -7179,6 +8297,7 @@ /Plasma_display /Plastic /Plastic_arts + /Plastic_pollution /Plastic_surgery /Plate_armour /Plate_tectonics @@ -7196,6 +8315,7 @@ /Playbill /Playground /Playing_card + /Playlist /Playoffs /PlayStation /PlayStation_2 @@ -7237,14 +8357,17 @@ /Polar_climate /Polar_regions_of_Earth /Polarizer + /Pole_star /Pole_vault /Poles /Police + /Police_dog /Police_officer /Policy /Policy_analysis /Policy_studies /Poliomyelitis + /Political_correctness /Political_criticism /Political_culture /Political_economy @@ -7260,9 +8383,9 @@ /Pollen /Pollination /Pollinator + /Pollutant /Pollution /Polo - /Polydeism /Polyethnicity /Polygon /Polyhedron @@ -7274,65 +8397,86 @@ /Polynomial /Polyphenol /Polyploid - /Polytheism /Pome /Pompeii /Pond /Pong /Pony + /Poodle + /Pooh%27s_Hunny_Hunt /Pool_of_Radiance + /Pop-culture_tourism + /Pop-Tarts /Pop_art /Pop_icon /Pop_music /Pop_punk + /Pop_rock /PopCap_Games + /Popcorn /Pope /Pope_Francis + /Popeye + /Popeyes /Popotan /Popular_culture /Popular_music /Population + /Population_decline /Population_density /Population_genetics + /Population_growth /Population_health /Populism + /Porcelain /Porcupine /Pork + /Pork_rind /Porky_Pig /Porpoise /Porsche /Porsche_911 /Port + /Port_Chicago_disaster + /Portable_media_player /Portugal /Positivism /Post-industrial_society /Post-structuralism /Postage_stamp + /Postcard + /Poster /Posthumanism + /Postmark + /Postmodern_architecture /Postmodern_art /Postmodernism /Postmodernity /Potassium /Potato + /Potato_chip + /Potbelly_Sandwich_Shop /Potential /Potential_energy /Potential_theory /Potential_well /Potluck /Potomac_River - /Potoo /Pottery /Poultry /Pound_sign /Pound_sterling /Poverty + /Powdered_sugar /Powell_Library + /Power-up /Power_electronics /Power_engineering /Power_outage /Power_Rangers /Power_station /Power_supply + /Powerade /Powerboating /PowerShell /Pragmatics @@ -7346,44 +8490,59 @@ /Praxeology /Prayer /Precedent + /Precious_metal /Precocial /Predation /Prediction /Prefabrication + /Prefrontal_cortex /Pregnancy /Prehistoric_Egypt /Prehistoric_warfare /Prehistory /Prejudice /Premier_League + /Premier_of_North_Korea /Premise /Premotor_cortex /Prequel /Preschool + /Present /Present_value - /Presenter /Preservationist /President + /Presidio /Press_Gang /Pressure /Pressure_gradient + /Pretzel /Price_index + /Pricing /Pride /Pride_and_Prejudice /Primary_care /Primary_color /Primary_education /Primary_energy + /Primary_source /Primate /Primatology /Prime_number + /Primeval_Whirl + /Prince + /Princess + /Principal_photography /Principle + /Pringles /Print_room /Printed_circuit_board /Printing /Printing_press /Printmaking /Prism + /Prison + /Prison_warden + /Prisoner_of_war /Privacy /Private_spaceflight /Probability @@ -7405,6 +8564,8 @@ /Professional /Professional_sports /Professor + /Professor_X + /Prognosis /Programmer /Programming_language /Progressive_Era @@ -7419,14 +8580,17 @@ /Proof_theory /Proofreading /Propaganda + /Propaganda_film /Propane /Property /Property_law + /Property_tax /Prophecy /Prophet /Proposition /Prose /Prose_Edda + /Prospecting /Prosthesis /Prosthodontics /Protagonist @@ -7440,9 +8604,11 @@ /Protestantism /Protist /Proton + /Prototype /Proverb /Proxemics /Proxy_voting + /Proxy_war /Prudence /Psephology /Pseudonym @@ -7452,6 +8618,7 @@ /Psychoanalysis /Psychodrama /Psycholinguistics + /Psychological_warfare /Psychologist /Psychology /Psychometrics @@ -7492,6 +8659,7 @@ /Puerto_Vallarta /Puffbird /Puffin + /Pug /Pulaski_Skyway /Pulford /Pulitzer_Prize @@ -7500,10 +8668,12 @@ /Pulmonology /Pulp_Fiction /Pump + /Pun /Punishment /Punk_rock /Puppet /Puppet_state + /Puppeteer /Puppetry /Pure_mathematics /Purgatory @@ -7544,6 +8714,7 @@ /Quantum_gravity /Quantum_machine /Quantum_mechanics + /Quantum_of_Solace /Quantum_simulator /Quartz /Quasi-War @@ -7556,6 +8727,7 @@ /Quenching /Quentin_Tarantino /Quercetin + /Quesadilla /Question /Question_answering /Question_mark @@ -7563,7 +8735,9 @@ /Queueing_theory /Quicksort /QuickTime + /Quilt /Quintic_function + /Quiznos /Quotation_mark /Quotient_ring /Quran @@ -7584,6 +8758,7 @@ /Racism /Rack_railway /Radar + /Radian /Radiant_energy /Radiation /Radiator_Springs_Racers @@ -7614,15 +8789,21 @@ /Radon /RAF_Menwith_Hill /Rag_doll + /Raging_Spirits + /Raiders_of_the_Lost_Ark /Rail_freight_transport /Rail_transport /Rail_transport_in_India + /Rail_transport_modelling /Railgun /Railroad_car /Railroad_engineer + /Railway_air_brake /Rain /Rainbow_trout /Rainforest + /Rainforest_Cafe + /Raisin /Raj_Reddy /Raja_Ravi_Varma /Rajiformes @@ -7659,6 +8840,8 @@ /Raymond_Berry /Raymond_Williams /Razor + /RC_Racer + /Reading_Rainbow /Reaganomics /Reagent /Real-time_strategy @@ -7675,23 +8858,29 @@ /Reason /Reasonable_doubt /Rebar + /Rebel_Alliance /Rebellion /Receptionist /Recession /Recipe + /Reconnaissance_satellite /Reconstruction_Era /Record_label + /Recorded_history /Records_management /Recreation /Recreation_area /Recreation_room + /Recreational_drug_use /Rectifier /Recto_and_verso /Recurvirostridae /Recycling + /Recycling_bin /Red /Red_blood_cell /Red_Bull + /Red_Car_Trolley /Red_Dead_Redemption /Red_deer /Red_Hot_Chili_Peppers @@ -7700,6 +8889,7 @@ /Red_rain_in_Kerala /Red_Sea /Red_Skelton + /Red_Square /Redox /Reducing_agent /Reductionism @@ -7707,9 +8897,11 @@ /Reefer_ship /Reese_Witherspoon /Reflection_nebula + /Reflections_of_China /Reform /Reform_movement /Refraction + /Refried_beans /Refrigeration /Refrigerator /Refugee @@ -7734,9 +8926,15 @@ /Religious_art /Religious_law /Religious_studies + /Religious_war /Relish + /Remake /Rembrandt + /Remembrance_Day + /Remix + /Remix_album /Renaissance + /Renaissance_architecture /Renaissance_art /Renaissance_Latin /Renewable_energy @@ -7744,17 +8942,22 @@ /Renminbi /Reptile /Research + /Reservoir /Resident_Evil_Zero + /Residential_area /Resignation /Resistor /Resort /Respect + /Respiratory_disease /Restaurant + /Restaurateur /Restriction_enzyme /Resurrection_of_Jesus /Retail - /Retina_Display + /Retina /Retirement_home + /Retro_style /Return_of_the_Jedi /Reuters /Revelation @@ -7771,9 +8974,15 @@ /Rhodes_University /Rhythm /Rhythm_game + /Rib_steak /Ribosome /Rice + /Rice_cake + /Rice_flour /Rice_milk + /Rice_noodles + /Rice_wine + /Rich_Text_Format /Richard_Cordray /Richard_Garriott /Richard_Nixon @@ -7798,24 +9007,31 @@ /Rings_of_Saturn /Rings_of_Uranus /Ringtone + /Rio_Carnival /Rio_de_Janeiro /Rio_Grande /Riot /Risk /Risk_management /Risky_Business + /Rite_of_passage /Ritual /River /River_delta /River_rapids_ride + /Riverboat + /Rivers_of_Light /RKO_Pictures /RMS_Titanic /RNA /Road /Road_pricing /Road_transport + /Roadside_attraction /Roaring_Rapids /Roaring_Twenties + /Roast_beef + /Roast_goose /Roasting /Robert_De_Niro /Robert_Downey_Jr. @@ -7835,12 +9051,17 @@ /Robotics_Institute /Rock_and_roll /Rock_Band + /Rock_candy /Rock_festival /Rock_music + /Rock_of_Gibraltar /Rock_opera + /Rockefeller_family /Rocket + /Rocket_artillery /Rocket_engine /Rocket_launch + /Rocket_Rods /Rockstar_Games /Rocky /Rocky_Mountains @@ -7860,14 +9081,17 @@ /Rolex /Roller /Roller_coaster + /Roller_coaster_inversion /Roller_derby /Roller_skates /Roller_skating + /Rolling_stock /Rolling_Stone /ROM_cartridge /Roman_aqueduct /Roman_army /Roman_art + /Roman_calendar /Roman_emperor /Roman_Empire /Roman_Forum @@ -7879,6 +9103,7 @@ /Romanian_language /Romanticism /Rome + /Romeo /Romeo_and_Juliet /Ron_Weasley /Ronald_Colman @@ -7887,6 +9112,7 @@ /Roof /Rookery_Building /Room + /Room_temperature /Roomba /Rooster /Root_beer @@ -7905,17 +9131,21 @@ /Rotation /Rotor_machine /Rotten_Tomatoes + /Rottweiler /Roulette - /Roundhouse /Routledge /Rowing /Roy_E._Disney + /Roy_O._Disney /Roy_of_the_Rovers /Royal_assent + /Royal_Collection /Royal_Dutch_Shell /Royal_family + /Royal_Institution /Royal_Navy /Royal_Society + /Rubik%27s_Cube /Ruble_sign /Rugby_football /Rugby_league @@ -7932,36 +9162,53 @@ /Russian_Civil_War /Russian_Empire /Russian_language + /Russian_Railways /Russian_Revolution /Russo_brothers /Ryan_Reynolds + /S%C3%A3o_Paulo /S.H.I.E.L.D. /Saber-toothed_cat + /Sabotage /Sacha_Baron_Cohen /Sacred /Sacred_king + /Sacrifice /Sadness + /Safari_park /Safety_standards /Saga /Sail /Sail_training /Sailboat /Sailfish + /Sailing_ship /Sailor /Saint_Lawrence + /Saint_Patrick%27s_Day /Saint_Petersburg /Saints_Row_IV /Sakharov_Prize /Salad + /Salad_cream /Salamander /Salary /Salem_witch_trials /Sales_tax + /Saliva /Salmon /Salt + /Salt-cured_meat + /Salt_and_pepper_shakers /Salt_lake + /Salt_Lake_City + /Saltbox + /Saltwater_crocodile + /Salvation /Samlesbury_witches /Sammy_Sosa + /Samoa + /Samosa /Sampling_bias /Samsung /Samuel_L._Jackson @@ -7992,6 +9239,8 @@ /Santa_Claus /Santosh_Sivan /SAP_SE + /Sapphire + /Sarcasm /SAT /Satellite /Satellite_dish @@ -8000,6 +9249,7 @@ /Satellite_television /Satire /Satoru_Iwata + /Saturated_fat /Saturday /Saturday_Night_Live /Saturn @@ -8014,9 +9264,12 @@ /Sauron /Sausage /Sausage_making + /Saut%C3%A9ing /Savanna /Saved_game + /Saving_Mr._Banks /Saving_Private_Ryan + /Savings_account /Saw /Sawfish /Saxbe_fix @@ -8025,6 +9278,7 @@ /Scaffolding /Scale_insect /Scale_model + /Scallion /Scallop /Scandinavia /Scandium @@ -8035,10 +9289,12 @@ /Scarlett_Johansson /Scattered_disc /Scattergories + /Scavenger_hunt /Scenario /Scenario_planning /Scenic_design /Scenography + /Scent_hound /Schizophrenia /Scholasticism /School @@ -8046,34 +9302,44 @@ /School_Daze /School_psychology /School_Rumble + /Schoolhouse_Rock! /Science /Science_education /Science_fiction + /Science_fiction_film /Science_museum + /Science_policy /Science_studies /Scientific_community /Scientific_journal /Scientific_method /Scientific_misconduct /Scientific_realism - /Scientific_revolution /Scientism /Scientist /Scientology /Scientometrics /Scissors + /Scooby-Doo + /Scooby-Doo!_Spooky_Games + /Scoobynatural + /Scorched_earth /Scorpion + /Scotch_Tape /Scotland /Scott_Forstall /Scottish_clan /Scottish_Parliament /Scottish_people + /Scottish_Reformation /Scrabble - /Screamer + /Scrambled_eggs /Screenshot /Screenwriter /Screw + /Scribal_abbreviation /Scripting_language + /Scrooge_McDuck /Scuba_diving /Scuba_set /Sculpture @@ -8083,24 +9349,33 @@ /Sea_ice /Sea_level_rise /Sea_lion + /Sea_monster /Sea_otter /Sea_slug /Sea_snail + /Sea_trial /Sea_urchin /Seabird + /Seafloor_spreading /Seafood /Sean_Connery /Search_and_rescue /Searchlight /Sears /Seashell + /Seaside_resort /Season + /Seating_capacity /Seattle + /Seawater + /Seaweed /SeaWorld /SeaWorld_Orlando + /SeaWorld_San_Diego /SECAM /Second /Second-order_cybernetics + /Second-wave_feminism /Second_World /Secondary_school /Secrecy @@ -8112,6 +9387,7 @@ /Section_sign /Secularity /Security + /Security_cameras /Sedimentary_rock /Sedimentology /Seduction @@ -8127,15 +9403,19 @@ /Sega_Saturn /Seinen_manga /Seinfeld + /Seismometer + /Selective_breeding /Selena /Selenium /Selenography /Self-actualization /Self-awareness /Self-care + /Self-censorship /Self-concept /Self-consciousness /Self-control + /Self-defense /Self-esteem /Self-harm /Self-help @@ -8143,14 +9423,16 @@ /Self-portrait /Self-realization /Self-reflection + /Self-sustainability /Selfie /Semantic_network /Semantic_Web /Semantics - /Semaphore_line /Semicolon /Semiconductor /Semigroup + /Seminar + /Seminole /Semiosis /Semiotics /Semiring @@ -8167,7 +9449,9 @@ /Separatory_funnel /September /Septic_tank + /Sequel /Sequence + /Sequoia_National_Park /Serbia /Serbian_Empire /Serena_Williams @@ -8175,11 +9459,13 @@ /Serotonin /Serval /Servomechanism + /Sesame_oil /Sesame_Street /Sesame_Workshop /Set_theory /Seth_MacFarlane /Seven_Dwarfs_Mine_Train + /Seven_Seas_Lagoon /Seven_Years%27_War /Severance_package /Severe_weather @@ -8187,10 +9473,14 @@ /Sewage /Sewage_treatment /Sewing + /Sewing_machine /Sewing_needle /Seymour_Papert /Shading /Shadow + /Shake_It_Off + /Shake_Shack + /Shakespearean_comedy /Shakira /Shakugan_no_Shana /Shamanism @@ -8198,13 +9488,18 @@ /Shanghai /Shanghai_Disney_Resort /Shanghai_Disneyland_Park + /Shanghai_Tower /Shape + /Shape_of_You /Share_taxi /Sharecropping + /Shared_universe /Shareholder /Shareware /Sharia /Shark + /Shark_attack + /Shark_Tank /Sharon_Tate /Sharp_Corporation /Shaving @@ -8213,31 +9508,45 @@ /Sheffer_stroke /Shekel_sign /Shelf_life + /Shellfish + /Shenandoah_National_Park /Sheng_Long + /Shenzhen /Sheriff_Woody /Sherlock_Holmes + /Sherman_Brothers /Sherman_Minton /Sheryl_Sandberg + /Shilling /Shin_Megami_Tensei /Shinto /Ship /Shipbuilding + /Shipwreck /Shirt /Shiva /Shoal + /Shock_absorber /Shock_wave /Shoe /Shoemaking /Sholay /Shoot /Shooter_game + /Shooting + /Shooting_sports + /Shoplifting + /Shopping_mall /Short_film /Short_story + /Short_ton /Shorthand + /Shorts /Shot_put /Shotgun /Shovel /Shovelware + /Show_business /Shower /Showrunner /Shrek @@ -8256,6 +9565,7 @@ /Sic /Sicilian_Baroque /Sickle + /Sickness_bag /Side_dish /Sideburns /Sidekick @@ -8267,6 +9577,7 @@ /Sightline /Sigi_Schmid /Sigmund_Freud + /Sign_language /Signal /Signal_processing /Sikhism @@ -8280,8 +9591,8 @@ /Silk /Silk_Road /Silly_Putty + /Silly_Symphony_Swings /Silver - /Simaroubaceae /SimCity /Simmering /Simple_group @@ -8299,6 +9610,7 @@ /Singapore_Airlines /Singing /SingStar + /Sinkhole /Sinology /Siphonophorae /Siri @@ -8315,11 +9627,13 @@ /Skateboard /Skateboarding /Skeletal_animation + /Skeletal_muscle /Skeleton /Skepticism /Sketch_comedy /Sketchpad /Ski_jumping + /Ski_resort /Skiing /Skill /Skimmer @@ -8328,8 +9642,10 @@ /Skipping_rope /Skirt /Skua + /Skull /Skullgirls /Skunk + /Sky /Skyfall /Skype /Skyscraper @@ -8340,18 +9656,27 @@ /Slavic_studies /Slayer /Sled + /Sled_dog /Sledge_hockey /Sleep /Sleep_hygiene /Sleep_medicine + /Sleeping_Beauty /Sleeping_Beauty_Castle + /Sleepwalking /Sleight_of_hand + /Slime_mold /Slinky + /Slinky_Dog_Dash + /Slinky_Dog_Zig_Zag_Spin /Slit_drum + /Slope + /Sloppy_joe /Slot_machine /Sloth /Slovenia /Slug + /Slum /Smart_card /Smart_grid /Smart_growth @@ -8363,21 +9688,28 @@ /Smartwatch /Smederevo_Fortress /Smelting + /Smithsonian_Institution /Smog + /Smoke_detector /Smoke_signal + /Smoked_meat /Smoking /Smoothie /Smoothness + /SMS /Snack /Snail /Snake /Snapchat /SNCF + /Snickers /Snipe /Snoop_Dogg /Snorri_Sturluson /Snow /Snow_leopard + /Snow_White + /Snow_White_Grotto /Snowball_Earth /Snowboarding /Snowmobile @@ -8388,7 +9720,6 @@ /Sochi /Social /Social_actions - /Social_animal /Social_behavior /Social_capital /Social_change @@ -8411,7 +9742,6 @@ /Social_organization /Social_philosophy /Social_policy - /Social_progress /Social_psychology /Social_relation /Social_research @@ -8457,6 +9787,7 @@ /Soil /Soil_biology /Soil_contamination + /Soil_erosion /Soil_science /Sojourner_Truth /Solar_cell @@ -8478,6 +9809,8 @@ /Solid_mechanics /Solidarity /Solitaire + /Solitary_confinement + /Solubility /Solution /Solvent /Somalia @@ -8490,6 +9823,7 @@ /Song /Song_dynasty /Songbird + /Songhai_Empire /Songwriter /Sonic_Jam /Sonic_Lost_World @@ -8500,6 +9834,7 @@ /Sony /Sony_Pictures /Sophocles + /Soprano /Sorting_algorithm /Soul_food /Soul_music @@ -8511,9 +9846,13 @@ /Sound_stage /SoundCloud /Soundness + /Sounds_Dangerous! /Soundtrack + /Soundtrack_album /Soup + /Sour_cream /Source_code + /Sourdough /South /South_Africa /South_America @@ -8541,17 +9880,24 @@ /Southern_Europe /Southern_Hemisphere /Southern_Ocean + /Southern_right_whale /Southern_United_States /Southwest_Airlines /Southwest_Territory + /Souvenir /Sovereign_state /Sovereignty /Soviet_Empire + /Soviet_people + /Soviet_space_program /Soviet_Union /Sowing /Soy_milk + /Soy_sauce /Soybean /Soyuz_11 + /Soyuz_programme + /Spa /Space /Space-based_economy /Space_Age @@ -8562,18 +9908,24 @@ /Space_heater /Space_Invaders /Space_Jam - /Space_logistics /Space_Mountain + /Space_Oddity + /Space_opera /Space_probe /Space_Race /Space_Shuttle + /Space_Shuttle_Endeavour + /Space_Shuttle_program /Space_station /Space_telescope /Space_tourism /Space_weapon /Spacecraft + /Spacecraft_propulsion /Spaceflight /Spaceport + /Spaceship_Earth + /SpaceShipTwo /Spacewar! /SpaceX /SpaceX_Dragon @@ -8585,11 +9937,14 @@ /Spanish_East_Indies /Spanish_Empire /Spanish_Florida + /Spanish_flu /Spanish_Inquisition /Spanish_language /Spanish_peseta + /Spare_ribs /Sparkling_wine /Sparrow + /Sparse_matrix /Sparta /Spatial_analysis /Spear @@ -8600,7 +9955,9 @@ /Speciation /Species /Spectator_sport + /SpectroMagic /Spectroscopy + /Speculation /Speculative_fiction /Speculative_reason /Speech @@ -8613,6 +9970,7 @@ /Speed_reading /Speed_skating /Speedrun + /Speedy_Gonzales /Speleology /Sperm_whale /Spermatogenesis @@ -8632,32 +9990,45 @@ /Spike_Lee /Spinach /Spinal_cord + /Spinning_jenny + /Spinning_mule + /Spinning_roller_coaster /Spinning_wheel /Spintronics + /Spire /Spirit /Spirit_Airlines /Spiritism + /Spiritual_successor /Spiritualism /Spirituality + /Spitzer_Space_Telescope /Splash_Mountain /Spoken_word /Sponge + /Sponge_cake + /SpongeBob_SquarePants /Spoon /Spoonbill + /Spork /Sport /Sport_psychology + /Sport_utility_vehicle /Sports_agent /Sports_car /Sports_club /Sports_commentator /Sports_drink /Sports_equipment + /Sports_game + /Sports_Illustrated /Sports_journalism /Sports_league /Sports_marketing /Sports_medicine /Sports_science /Sports_venue + /SportsCenter /Sportsmanship /Spotify /Spotted_hyena @@ -8668,7 +10039,9 @@ /Squall_line /Square /Square_Enix + /Square_rig /Square_root + /Squeaky_toy /Squid /Squirrel /Sri_Lanka @@ -8680,19 +10053,21 @@ /Stagecoach /Stagecraft /Stain + /Stained_glass /Stainless_steel /Stairs /Stairway_to_Heaven /Stallion /Stamp_Act_1765 /Stamp_Act_Congress + /Stamp_collecting /Stampede /Stamper_brothers /Stan_Freberg /Stan_Lee /Stan_Musial + /Stand-up_comedy /Standard_deviation - /Standard_gauge /Standard_Oil /Standardization /Stanford_University @@ -8705,8 +10080,12 @@ /Star_Trek /Star_Trek_Beyond /Star_Wars + /Star_Wars_Hotel + /Star_Wars_Launch_Bay /Star_Wars_Rebels + /Star_Wars_Weekends /Starbucks + /Starch /StarCraft /Starfish /Stark_Raving_Dad @@ -8719,12 +10098,17 @@ /State_media /State_of_matter /State_school + /Staten_Island /Staten_Island_Ferry + /States%27_rights /Station_wagon /Statistics /Statue_of_Liberty /Status_of_Jerusalem /Statute + /Steak + /Steak_%27n_Shake + /Steak_tartare /Stealth_game /Stealth_technology /Steam_engine @@ -8734,11 +10118,12 @@ /Steamboat_Willie /Steampunk /Steel + /Steel_roller_coaster /Steganography /Stegosaurus - /Steins;Gate /Stellar_evolution /Stem_cell + /Stencil /Stephen_Colbert /Stephen_Curry /Stephen_Hawking @@ -8746,21 +10131,28 @@ /Stereoscopy /Stereotype /Stereotype_threat + /Sterling_silver /Steroid /Steve_Carell /Steve_Jobs /Steve_Martin /Steve_Wozniak /Steven_Spielberg + /Stevie_Wonder /Stew + /Stewardship /Stewie_Griffin /Stilt /Stilts + /Stimulation /Stingray /Stir_frying /Stirling_engine + /Stitch%27s_Great_Escape! + /Stitch_Encounter /Stoat /Stochastic_process + /Stock /Stock_exchange /Stock_market /Stock_market_crash @@ -8768,10 +10160,13 @@ /Stock_trader /Stockbroker /Stockholm + /Stocks /Stoicism + /Stomach /Stone-curlew /Stone_Age /Stone_Ghost + /Stone_tool /Stonehenge /Stonewall_Inn /Stonewall_riots @@ -8779,10 +10174,12 @@ /Stop_motion /Stork /Storm + /Storm_drain /Storm_surge /Stormwater /Story_arc /Storytelling + /Stout /Stove /Strait /Straitjacket @@ -8791,16 +10188,19 @@ /Strategy /Strategy_game /Strategy_guide + /Stratosphere /Straw /Strawberry /Strawberry_Panic! /Stream /Streaming_media + /Streaming_television /Street /Street_art /Street_food /Street_football /Street_hockey + /Street_light /Street_magic /Street_performance /Street_racing @@ -8810,24 +10210,34 @@ /String_instrument /String_theory /Striped_polecat + /Stroke /Strontium /Structural_biology /Structuralism /Structure - /Struthionidae /Student /Student_society + /Studio + /Studio_Backlot_Tour + /Stuffed_toy + /Stuffing /Stunt_performer /Sturgeon /Stuttering + /Styx + /Sub-Saharan_Africa + /Subatomic_particle /Subculture /Subgroup /Subjectivism + /Subjectivity /Submarine + /Submarine_Voyage /Subregion /Subsidiary /Subsidy /Subsistence_agriculture + /Subspecies /Substance_abuse /Substance_theory /Subtraction @@ -8836,10 +10246,11 @@ /Sucrose /Sudan /Sudoku + /Suffering /Sugar /Sugar_glider + /Sugar_substitute /Sugarcane - /Suger /Sui_dynasty /Sui_generis /Suicide @@ -8849,6 +10260,7 @@ /Sultan /Sumatra /Sumer + /Summer_Nightastic! /Summer_Olympic_Games /Summer_Paralympic_Games /Sumo @@ -8875,6 +10287,7 @@ /Super_Size_Me /Super_Smash_Bros. /Super_Smash_Bros._Brawl + /Super_Smash_Bros._Melee /Supercomputer /Superconductivity /Supercontinent @@ -8890,13 +10303,17 @@ /Supernatural /Supernova /Superpower + /Superstar_Limo + /Superstition /Supervillain /Supervolcano /Supper /Supply_and_demand /Supply_chain + /Support_group /Surf_music /Surface_area + /Surface_mining /Surface_runoff /Surface_science /Surface_tension @@ -8917,22 +10334,29 @@ /Sustain /Sustainability /Sustainable_agriculture + /Sustainable_architecture /Sustainable_design + /Sustainable_development + /Sustainable_tourism /Swallow /Swamp /Swan /Swarm_robotics - /Swaziland /Sweden /Swedish_Empire + /Sweet_Child_o%27_Mine /Sweet_corn + /Sweet_Home_Alabama /Sweet_potato + /Sweet_soy_sauce /Swift + /Swimming /Swimming_pool /Swimsuit /Swing_bridge /Swing_music /Swing_state + /Swiss_Family_Treehouse /Switch /Switzerland /Sword @@ -8961,6 +10385,7 @@ /Syria /Syrian_Air_Force /Syrian_Civil_War + /Syrup /System /System_dynamics /System_Shock @@ -8974,8 +10399,12 @@ /Systems_psychology /Systems_science /Systems_theory + /T-bone_steak /T-Mobile_US + /T-shirt /Taare_Zameen_Par + /Tabasco + /Tabasco_pepper /Table_apple /Table_football /Table_Mountain @@ -8984,16 +10413,21 @@ /Tabletop_game /Tableware /Taboo + /Taco /Taco_Bell + /Taekwondo /Taft_Commission /Tagus /Tahiti + /Tahitian_Terrace /Tailor /Taipei /Taipei_101 /Taiwan /Taj_Mahal /Tajikistan + /Take-out + /Take_On_Me /Takeover /Tales_of_Graces /Tales_of_Hearts @@ -9007,6 +10441,7 @@ /Tampa_Bay /Tanager /Tangent + /Tangled /Tank /Tank_locomotive /Tansu @@ -9017,12 +10452,18 @@ /Tapas /Tape_recorder /Tapestry + /Tapestry_of_Nations /Tarantula /Target_Corporation + /Tarot /Tarzan + /Tarzan%27s_Treehouse /Task_analysis /Taste + /Taste_bud /Tattoo + /Tax + /Tax_evasion /Tax_incidence /Tax_law /Taxicab @@ -9032,31 +10473,39 @@ /Tcl /Te_Rauparaha /Tea + /Tea_bag /Teacher + /Teacup /Team /Team_building /Team_Disney /Team_Fortress_2 /Team_sport + /Teapot /Teasing + /Teatro_Col%C3%B3n /TechCrunch /Technical_director /Technical_drawing /Technical_support /Technicolor + /Techno /Technocapitalism /Technocriticism + /Technological_change /Technological_evolution /Technology - /Technology_evangelist /Tectonics /Ted_Cruz /Teddy_bear + /Tel_Aviv /Telecommunication /Telegraphy /Telemetry + /Telenovela /Teleology /Telephone + /Teleportation /Teleprinter /Telescope /Television @@ -9076,6 +10525,7 @@ /Temporal_finitism /Ten_Commandments /Tender_Mercies + /Tendon /Tennessee /Tennis /Tenor @@ -9084,8 +10534,10 @@ /Tensor_algebra /Tensor_field /Tent + /Tentacle /Teradata /Teratology + /Teriyaki /Term_logic /Terminology /Termite @@ -9095,16 +10547,21 @@ /Terracotta_Army /Terrestrial_animal /Terrestrial_planet + /Terrier /Territory_of_Alaska /Terry_Sanford /Tertiary_education + /Tesla_coil + /Tesla_Factory /Tesla_Model_S /Tesla_Powerwall + /Test_pilot /Test_Track /Test_tube /Testosterone /Tetrahedron /Tetris + /TeX /Texas /Texas_annexation /Texas_Instruments @@ -9115,12 +10572,15 @@ /Textile /Textile_arts /Textile_design + /Textile_industry /Texture_mapping + /TGI_Fridays /TGV /Thai_art /Thai_baht /Thailand /Thanet_Wind_Farm + /Thanksgiving /Thanos /Thatching /The_A-Team @@ -9128,6 +10588,7 @@ /The_Archers /The_arts /The_Auk + /The_Barnstormer /The_Beatles /The_Big_Bang_Theory /The_Black_Mages @@ -9135,18 +10596,22 @@ /The_Clash /The_Colbert_Report /The_Daily_Show + /The_Dapper_Dans /The_Dark_Knight_Rises /The_Departed /The_Empire_Strikes_Back /The_Fifth_Element /The_finger /The_Godfather + /The_Golden_Mickeys /The_Good_Dinosaur /The_Great_Gatsby + /The_Great_Movie_Ride /The_Guardian /The_Hall_of_Presidents /The_Hangover /The_Hateful_Eight + /The_Haunted_Mansion /The_Hobbit /The_Home_Depot /The_Hurt_Locker @@ -9158,6 +10623,7 @@ /The_Life_of_Pablo /The_Lion_King /The_Lord_of_the_Rings + /The_Making_of_Me /The_Matrix /The_Matrix_Reloaded /The_Muppets @@ -9172,29 +10638,37 @@ /The_Province /The_Return_of_the_King /The_Rolling_Stones + /The_Scooby-Doo_Show /The_Shadow /The_Signpost /The_Simpsons /The_Simpsons_Game /The_Simpsons_Movie /The_Sims + /The_Sims_3 /The_Social_Network /The_Sopranos + /The_Spirit_of_Pocahontas /The_Stolen_Earth /The_Supremes /The_Tempest /The_Terminator /The_Thinker /The_Time_Tunnel + /The_Timekeeper /The_Twilight_Zone /The_Wall_Street_Journal /The_Walt_Disney_Company + /The_Walt_Disney_Story /The_Washington_Post /The_West_Wing /The_Wire + /The_World_Beneath_Us /Theatre /Theatre_director + /Theatrical_property /Theism + /Theme_music /Theodor_W._Adorno /Theodore_Roosevelt /Theology @@ -9205,7 +10679,6 @@ /Theory /Theory_of_forms /Theory_of_mind - /Theosophy /Thermal_conduction /Thermal_energy /Thermal_radiation @@ -9218,6 +10691,7 @@ /Thesaurus /Thetis /Thiamine + /Thimble /Think_different /Think_tank /Third_World @@ -9232,13 +10706,17 @@ /Thought /Thought_experiment /Threatened_species + /Three-dimensional_space /Three_Gorges_Dam /Threskiornithidae /Thrift_Shop + /Throat /Throne + /Thrust /Thunderstorm /Thursday /THX + /Thymus /Tian_Shan /Tibet /Tibetan_art @@ -9246,15 +10724,21 @@ /Tichborne_case /Ticker_symbol /Tidal_power + /Tide /Tie-dye + /TIE_fighter /Tied-arch_bridge /Tied_island /Tiger /Tiger_shark + /Tigger /Tightrope_walking /Tigris + /Tijuana /Tiki + /Tiki_culture /Tilde + /Tile /Tim_Burton /Tim_Cook /Tim_Duncan @@ -9266,8 +10750,10 @@ /Time_Inc. /Time_to_Get_Tough /Time_travel - /Time_Warner /Time_zone + /Timeline + /Times_Square + /Timon_and_Pumbaa /Tin /Tin_can /Tin_Toy @@ -9278,20 +10764,25 @@ /Tire /Tironian_notes /Tissue_engineering + /Tissue_paper /Titanfall /Titanium /Titration /Tlingit + /TNT /TNT_equivalent + /To_be_announced /To_Kill_a_Mockingbird /To_Pimp_a_Butterfly /Toad + /Toast /Toaster /Tobacco_smoking /Todd_Manning /Toffee /Tofu /Toilet + /Token_coin /Tokyo /Tokyo_Disney_Resort /Tokyo_Disneyland @@ -9303,34 +10794,50 @@ /Tom_Hanks /Tom_Hardy /Tom_Hiddleston + /Tom_Sawyer_Island + /Tomato + /Tomato_sauce /Tommy_Lee_Jones + /Tomorrow_Never_Dies /Tomorrowland + /Ton + /Tonga + /Tongue /Tonne + /Tony_Award + /Tony_Baxter /Tool /Toothpaste /Top-level_domain /Top_Gun /Top_hat + /Topaz + /Topiary /Topological_group /Topology /Topos + /Topping_out /Tornado /Toronto /Torpedo /Torque /Tort + /Tortilla /Tortoise /Toshiba /Totalitarianism /Toucan /Touchscreen /Tour_de_France + /Tour_guide /Tourette_syndrome /Tourism /Tourism_geography + /Tourist_attraction /Tournament /Tower /Tower_defense + /Tower_of_London /Town /Town_square /Toxic_waste @@ -9343,6 +10850,8 @@ /Toy_Story_4 /Toy_Story_Land /Toy_Story_Midway_Mania! + /Toy_Story_of_Terror! + /Toy_Story_Racer /Toy_train /Toyota /Toyota_Corolla @@ -9350,14 +10859,19 @@ /Toys_%22R%22_Us /Track_and_field /Track_gauge + /Trackball /Trackless_train /Tracksuit /Tractor /Trade + /Trade_fair + /Trade_route + /Trade_union /Trademark /Trademark_symbol /Trader_Joe%27s /Tradesman + /Trading_card /Trading_post /Tradition /Traditional_animation @@ -9369,19 +10883,26 @@ /Trail /Train /Train_station + /Training /Trait_theory + /Trajan%27s_Column + /Trajectory /Tram /Trampoline + /Transaction_cost /Transceiver /Transcendentalism /Transducer + /Transfer_window /Transformer + /Transformers /Transhumanism /Transistor /Transistor_radio /Transit_of_Mercury /Transit_of_Venus /Translation + /Transmitter /Transport /Transport_law /Transport_layer @@ -9398,15 +10919,19 @@ /Travel_literature /Traveling_carnival /Treason + /Treasure_Planet /Treasurer /Treatise + /Treaty_of_Tordesillas /Tree /Tree_frog /Tree_kingfisher /Treecreeper /Trench_warfare /Trentino + /Trespass /Trestle_bridge + /Trevor_Noah /Trial /Trial_and_error /Tribal_chief @@ -9414,6 +10939,10 @@ /Tribology /Triborough_Bridge /Tribune_Media + /Tribune_Publishing + /TriceraTop_Spin + /Triceratops + /Trick-taking_game /Trickster /Trigonometric_functions /Trigonometry @@ -9426,6 +10955,7 @@ /Trogon /Trojan_Horse /Trojan_War + /Troll /Trolley_park /Trolleybus /Trombone @@ -9480,6 +11010,7 @@ /Turn-based_strategy /Turnkey /Turtle + /Turtle_Talk_with_Crush /Tuya /Twelve_Years_a_Slave /Twenty_One_Pilots @@ -9497,6 +11028,7 @@ /Typography /Tyranni /Tyrannosaurus + /Tyrannosaurus_rex /Tyrant_flycatcher /Tyrone_Wheatley /U-boat @@ -9516,21 +11048,28 @@ /Uncertainty /Uncle_Sam /Unconscious_mind + /Unconsciousness /Under_Armour + /Under_Pressure /Undergarment + /Undergraduate_education /Underground_comix /Underscore /Understanding /Undertale + /Underwater_diving /Underwater_sports /Underworld /Unemployment /UNESCO + /Unexploded_ordnance + /UNICEF /Unicode /Unicode_symbols /Unicorn /Unicycle /Unicycle_hockey + /Unification_of_Germany /Uniform /Uniformitarianism /Union_Army @@ -9538,16 +11077,24 @@ /Unit_circle /Unitarianism /United_Airlines + /United_Arab_Emirates /United_Kingdom /United_Nations + /United_Parcel_Service /United_States /United_States_Army + /United_States_dollar /United_States_Navy + /United_States_passport + /United_States_patent_law + /United_States_Senate /Universal_algebra + /Universal_Media_Disc /Universal_Orlando /Universal_Pictures /Universal_Time /Universe + /Universe_of_Energy /University /University_of_Cambridge /Unix @@ -9556,10 +11103,14 @@ /Unobservable /Unreal_Tournament /Unreliable_narrator + /Unsupervised_learning + /Upper_class /Uppsala_Cathedral /UPS_Airlines /Upstate_New_York + /Uptown_Funk /Uranium + /Uranium-235 /Uranus /Urbain_Le_Verrier /Urban_agriculture @@ -9575,6 +11126,7 @@ /Urban_geography /Urban_legend /Urban_planning + /Urban_renewal /Urban_sociology /Urban_sprawl /Urban_studies @@ -9592,18 +11144,21 @@ /Usenet_newsgroup /User_agent /User_interface + /User_interface_design /Utah /Utilitarianism /Utility + /Utility_tunnel + /Utopia /Uzbekistan /V_for_Vendetta + /Vaccination /Vaccine /Vacuum /Vacuum_cleaner /Vacuum_tube /Valentine%27s_Day /Valet - /Validity /Valley /Value_theory /Valve_Corporation @@ -9613,11 +11168,14 @@ /Van /Van_der_Waals_force /Vancouver + /Vandalism /Vanga /Vanilla /Vanir /Vapor + /Vapor_pressure /Vaquita + /Variable-message_sign /Variance /Variety_show /Vatican_City @@ -9628,19 +11186,23 @@ /Vega_program /Veganism /Vegetable + /Vegetable_oil /Vegetarianism /Vegetation + /Veggie_burger /Vehicle /Vehicle_dynamics /Vehicle_simulation_game /Velcro /Velociraptor + /Vending_machine /Venera /Venezuela /Venice /Venice_Biennale /Venice_Film_Festival /Venn_diagram + /Venom /VentureBeat /Venus /Venus_Express @@ -9652,9 +11214,9 @@ /Vermont /Vernacular /Vernor_Vinge - /Versine /Vertebrate /Vertical_bar + /Veterans_Day /VHS /Viacom /Viaduct @@ -9665,6 +11227,7 @@ /Victory_garden /Video /Video_art + /Video_CD /Video_game /Video_game_art /Video_game_console @@ -9677,6 +11240,7 @@ /Video_game_producer /Video_game_programmer /Video_on_demand + /Videocassette_recorder /Viduidae /Vienna /Vienna_Circle @@ -9691,10 +11255,12 @@ /Villa /Villa_Massimo /Village + /Villain /Vincent_van_Gogh /Vine /Vinegar /Vint_Cerf + /Vintage_car /Vinyl_roof /Viola /Violin @@ -9753,6 +11319,8 @@ /Volumetric_flask /Volunteering /Volvo + /Voting + /Vowel /Vox_Media /Voyager_1 /Voyager_2 @@ -9762,6 +11330,7 @@ /Wakamaru /Wales /Walking + /Walking_stick /Walkman /Wall /WALL-E @@ -9770,6 +11339,7 @@ /Wallachia /Wallcreeper /Walmart + /Walnut /Walrus /Walt_Disney /Walt_Disney_Imagineering @@ -9777,9 +11347,12 @@ /Walt_Disney_Records /Walt_Disney_Studios_Park /Walt_Disney_World + /Walt_Disney_World_Resort /Walter_Camp /Walter_Payton /Walter_Pitts + /Wannsee_Conference + /Wanzhou_District /War /War_film /War_on_drugs @@ -9787,24 +11360,31 @@ /Warbler /Warcraft /Warehouse + /Wargame /Wario + /Warlord /Warner_Bros. /Warren_Buffett /Warren_Court /Warren_Sturgis_McCulloch /Warring_States_period /Warrior + /Warsaw /Warsaw_Pact /Warship /Warwick_Davis + /Wasabi /Washing_machine /Washington_Metro + /Washington_Monument /Wasp /Waste + /Waste_heat /Waste_management /Wastewater /Watch /Watchmen + /WatchOS /Water /Water_buffalo /Water_chevrotain @@ -9812,10 +11392,12 @@ /Water_park /Water_pollution /Water_purification + /Water_quality /Water_resources /Water_scarcity /Water_slide /Water_supply + /Water_taxi /Water_tower /Water_treatment /Water_turbine @@ -9829,13 +11411,14 @@ /Watergate_complex /Watergate_scandal /Watermark + /Watermelon /Waterway /Watt - /Wattle-eye /Wave /Wave_farm /Wave_pool /Wave_power + /Waveform /Waveguide /Wavelength /Wax @@ -9844,6 +11427,9 @@ /Wayback_Machine /Waymo /Waze + /We_Are_the_Champions + /We_will_bury_you + /We_Will_Rock_You /Wealth /Weapon /Wearable_computer @@ -9870,8 +11456,10 @@ /Website /Wedding /Wednesday + /WEDway_people_mover /Week /Wehrmacht + /Weighing_scale /Weight /Welding /Welfare_economics @@ -9904,11 +11492,14 @@ /Whale /WhatsApp /Wheat + /Wheat_flour /Wheel /Wheelbarrow /Wheelchair /Wheelchair_basketball /Wheelchair_netball + /Whey + /Whipped_cream /Whippet /Whiskers /Whisky @@ -9924,19 +11515,25 @@ /White_wine /Whiteness_studies /Whole_Foods_Market + /Whole_grain /Whole_language /Whoopi_Goldberg /Whooping_crane /WHSmith /Wi-Fi + /Wide-body_aircraft /Wide_area_network + /Wide_release /Wiffle_ball /Wii /Wii_Fit /Wii_Play + /Wii_Remote /Wii_Sports /Wii_U + /Wiki /WikiLeaks + /Wikimedia_Foundation /Wikipedia /Wil_Wheaton /Wilco @@ -9949,6 +11546,7 @@ /Wildfire /Wildlife /Wildlife_corridor + /Wildlife_Express_Train /William_Howard_Taft /Willis_Tower /Wind @@ -9968,6 +11566,7 @@ /Windows_XP /Windsurfing /Wine + /Wine_bottle /Wine_in_China /Wine_tasting /Winery @@ -9975,6 +11574,7 @@ /Winston_Churchill /Winter_Olympic_Games /Winter_Paralympic_Games + /Winter_solstice /Winter_sport /Wire /Wire-frame_model @@ -9983,26 +11583,34 @@ /Wireless_network /Wisconsin /Wisdom + /Wit /Witchcraft /Wiz_Khalifa /Wizarding_World + /Wolf /Wolfdog + /Wolfram_Mathematica /Wolverine /Woman /Wombat + /Women%27s_history /Women%27s_rights /Women%27s_suffrage /Women_artists /Women_in_science + /Women_in_STEM_fields /Won_sign /Wonder_Twins /Wonder_Woman + /Wonders_of_China + /Wonders_of_Life /Wonders_of_the_World /WonderSwan /Wood - /Wood_hoopoe /Wood_lemming /Wood_veneer + /Woodcut + /Wooden_roller_coaster /Woodpecker /Woodrow_Wilson /Woodwind_instrument @@ -10018,10 +11626,14 @@ /Word_of_mouth /Word_processor /Work_hardening + /Workhouse /Working_class + /Working_dog /Working_memory /World + /World%27s_fair /World_Bank + /World_Chess_Championship /World_cinema /World_economy /World_government @@ -10030,8 +11642,10 @@ /World_literature /World_map /World_of_Color + /World_of_Motion /World_population /World_Series + /World_Trade_Organization /World_view /World_war /World_War_I @@ -10040,9 +11654,11 @@ /Worm /Wreck-It_Ralph /Wren + /Wrestler /Wrestling /Wright_brothers /Wright_Model_A + /Wrigley_Company /Wrigley_Field /Writer /Writing @@ -10050,6 +11666,7 @@ /Written_Chinese /Written_language /Wrongdoing + /Wrought_iron /Wyoming /X-Men /X-ray @@ -10058,6 +11675,7 @@ /Xbox_360 /Xbox_Live /Xbox_One + /Xcode /Xenogears /Xenon /Xenon_arc_lamp @@ -10075,17 +11693,22 @@ /Yahoo! /Yahtzee /Yale_University + /Yankee /Yasunori_Mitsuda + /Year /Yeast + /Yeezus /Yellow /Yelp /Yemen /Yeti /Yin_and_yang /Yo-yo + /Yoda /Yoga /Yogurt /Yoko_Shimomura + /Yolk /Yom_Kippur /York /Yosemite_National_Park @@ -10106,6 +11729,7 @@ /Zero_of_a_function /Zeus /Zinc + /Zion_National_Park /Zipper /Zipper_storage_bag /Zirconium @@ -10119,6 +11743,5 @@ /Zootopia /Zoroastrianism /ZX_Spectrum - /PC_LOAD_LETTER diff --git a/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist b/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist index 390cdc6..ab06220 100644 --- a/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist +++ b/WKRKit/WKRKit/Constants/WKRKitConstants-TESTING_ONLY.plist @@ -35,5 +35,9 @@ 2 BonusPointsInterval 120 + MaxGlobalRacePlayers + 4 + MaxLocalRacePlayers + 8 diff --git a/WKRKit/WKRKit/Constants/WKRKitConstants.plist b/WKRKit/WKRKit/Constants/WKRKitConstants.plist index fb684ad..1d828df 100644 --- a/WKRKit/WKRKit/Constants/WKRKitConstants.plist +++ b/WKRKit/WKRKit/Constants/WKRKitConstants.plist @@ -10,6 +10,7 @@ org/wiki/Portal: org/wiki/Template: org/wiki/Help: + org/wiki/Category: /File: #/ @@ -26,7 +27,7 @@ RandomURLString https://en.m.wikipedia.org/wiki/Special:Random Version - 15 + 20 WhatLinksHereURLString https://en.m.wikipedia.org/w/index.php?title=Special:WhatLinksHere MaxFoundPagePlayers @@ -37,5 +38,9 @@ 2 BonusPointsInterval 120 + MaxGlobalRacePlayers + 4 + MaxLocalRacePlayers + 8 diff --git a/WKRKit/WKRKit/Constants/WKRKitConstants.swift b/WKRKit/WKRKit/Constants/WKRKitConstants.swift index 2e1c139..7b4ed32 100644 --- a/WKRKit/WKRKit/Constants/WKRKitConstants.swift +++ b/WKRKit/WKRKit/Constants/WKRKitConstants.swift @@ -16,7 +16,7 @@ public struct WKRKitConstants { public let version: Int public static var current = WKRKitConstants() - internal let quickRace: Bool + internal let isQuickRaceMode: Bool public let connectionTestTimeout: Double internal let pageTitleStringToReplace: String @@ -34,6 +34,9 @@ public struct WKRKitConstants { internal let bannedURLFragments: [String] + public let maxGlobalRacePlayers: Int + public let maxLocalRacePlayers: Int + // MARK: - Initalization //swiftlint:disable:next cyclomatic_complexity function_body_length @@ -83,9 +86,15 @@ public struct WKRKitConstants { guard let bannedURLFragments = documentsConstants["BannedURLFragments"] as? [String] else { fatalError("WKRKitConstants: No BannedURLFragments value") } + guard let maxGlobalRacePlayers = documentsConstants["MaxGlobalRacePlayers"] as? Int else { + fatalError("WKRKitConstants: No MaxGlobalRacePlayers value") + } + guard let maxLocalRacePlayers = documentsConstants["MaxLocalRacePlayers"] as? Int else { + fatalError("WKRKitConstants: No MaxLocalRacePlayers value") + } self.version = version - self.quickRace = quickRace + self.isQuickRaceMode = quickRace self.connectionTestTimeout = connectionTestTimeout self.pageTitleStringToReplace = pageTitleStringToReplace @@ -102,11 +111,13 @@ public struct WKRKitConstants { self.votingArticlesCount = votingArticlesCount self.bannedURLFragments = bannedURLFragments + self.maxGlobalRacePlayers = maxGlobalRacePlayers + self.maxLocalRacePlayers = maxLocalRacePlayers } // MARK: - Helpers - @available(*, deprecated, message: "Only for debugging") + @available(*, deprecated, message: "Only for testing") static public func removeConstants() { let fileManager = FileManager.default @@ -123,7 +134,7 @@ public struct WKRKitConstants { } } - @available(*, deprecated, message: "Only for debugging") + @available(*, deprecated, message: "Only for testing") static public func updateConstantsForTestingCharacterClipping() { copyBundledResourcesToDocuments(constantsFileName: "WKRKitConstants-TESTING_ONLY") } @@ -143,16 +154,16 @@ public struct WKRKitConstants { return } - guard let recordConstantsAsset = record["ConstantsFile"] as? CKAsset, - let recordArticlesAsset = record["ArticlesFile"] as? CKAsset, - let recordGetLinksScriptAsset = record["GetLinksScriptFile"] as? CKAsset else { + guard let recordConstantsAssetURL = (record["ConstantsFile"] as? CKAsset)?.fileURL, + let recordArticlesAssetURL = (record["ArticlesFile"] as? CKAsset)?.fileURL, + let recordGetLinksScriptAssetURL = (record["GetLinksScriptFile"] as? CKAsset)?.fileURL else { return } DispatchQueue.main.async { - copyIfNewer(newConstantsFileURL: recordConstantsAsset.fileURL, - newArticlesFileURL: recordArticlesAsset.fileURL, - newGetLinksScriptFileURL: recordGetLinksScriptAsset.fileURL) + copyIfNewer(newConstantsFileURL: recordConstantsAssetURL, + newArticlesFileURL: recordArticlesAssetURL, + newGetLinksScriptFileURL: recordGetLinksScriptAssetURL) } } } @@ -173,6 +184,12 @@ public struct WKRKitConstants { return } + if !FileManager.default.fileExists(atPath: documentsDirectory.path) { + try? FileManager.default.createDirectory(at: documentsDirectory, + withIntermediateDirectories: false, + attributes: nil) + } + let documentsArticlesURL = documentsDirectory.appendingPathComponent("WKRArticlesData.plist") let documentsConstantsURL = documentsDirectory.appendingPathComponent("WKRKitConstants.plist") let documentsGetLinksScriptURL = documentsDirectory.appendingPathComponent("WKRGetLinks.js") @@ -221,7 +238,7 @@ public struct WKRKitConstants { newGetLinksScriptFileURL: bundledGetLinksScriptURL) } - internal func finalArticles() -> [String] { + lazy private(set) var finalArticles: [String] = { //swiftlint:disable:next line_length guard let documentsArticlesURL = FileManager.default.documentsDirectory?.appendingPathComponent("WKRArticlesData.plist"), let arrayFromURL = NSArray(contentsOf: documentsArticlesURL), @@ -229,7 +246,7 @@ public struct WKRKitConstants { fatalError("Failed to load articles plist") } return array - } + }() internal func getLinksScript() -> String { guard let documentsScriptURL = FileManager.default.documentsDirectory?.appendingPathComponent("WKRGetLinks.js"), diff --git a/WKRKit/WKRKit/Game/WKRGame.swift b/WKRKit/WKRKit/Game/WKRGame.swift index b861699..57771a1 100644 --- a/WKRKit/WKRKit/Game/WKRGame.swift +++ b/WKRKit/WKRKit/Game/WKRGame.swift @@ -10,15 +10,19 @@ import Foundation public class WKRGame { - // MARK: - Closures - - var bonusPointsUpdated: ((Int) -> Void)? + // MARK: - Types + + enum ListenerUpdate { + case bonusPoints(Int) + case playersReadyForNextRound + case readyStates(WKRReadyStates) + case hostResults(WKRResultsInfo) + case localResults(WKRResultsInfo) + } - var allPlayersReadyForNextRound: (() -> Void)? - var readyStatesUpdated: ((WKRReadyStates) -> Void)? + // MARK: - Closures - var hostResultsCreated: ((WKRResultsInfo) -> Void)? - var localResultsUpdated: ((WKRResultsInfo) -> Void)? + var listenerUpdate: ((ListenerUpdate) -> Void)? // MARK: - Properties @@ -55,19 +59,30 @@ public class WKRGame { bonusTimer?.invalidate() bonusTimer = Timer.scheduledTimer(withTimeInterval: WKRKitConstants.current.bonusPointsInterval, repeats: true) { [weak self] _ in + guard let self = self else { return } //swiftlint:disable:next line_length - self?.activeRace?.bonusPoints += WKRKitConstants.current.bonusPointReward - if let points = self?.activeRace?.bonusPoints { - self?.bonusPointsUpdated?(points) + self.activeRace?.bonusPoints += WKRKitConstants.current.bonusPointReward + if let points = self.activeRace?.bonusPoints { + self.listenerUpdate?(.bonusPoints(points)) } } } } - func createRaceConfig() -> WKRRaceConfig? { + func createRaceConfig() -> (config: WKRRaceConfig?, logEvent: WKRLogEvent?)? { guard localPlayer.isHost else { fatalError("Local player not host") } - return preRaceConfig?.raceConfig() + + var sessionPoints = calculateSessionPoints() + + // include players that have no points yet + let votingPlayers = players + .filter { $0.state == .voting } + .map { $0.profile } + for player in votingPlayers where sessionPoints[player] == nil { + sessionPoints[player] = 0 + } + return preRaceConfig?.raceConfig(with: sessionPoints) } func finishedRace() { @@ -94,7 +109,7 @@ public class WKRGame { // MARK: - Player States internal func playerUpdated(_ player: WKRPlayer) { - if let index = players.index(of: player) { + if let index = players.firstIndex(of: player) { players[index] = player } else { players.append(player) @@ -111,15 +126,45 @@ public class WKRGame { } let readyStates = WKRReadyStates(players: players) - readyStatesUpdated?(readyStates) - if localPlayer.isHost && readyStates.isReadyForNextRound { - allPlayersReadyForNextRound?() + listenerUpdate?(.readyStates(readyStates)) + if localPlayer.isHost, + let racePlayers = completedRaces.last?.players, + readyStates.areAllRacePlayersReady(racePlayers: racePlayers) { + listenerUpdate?(.playersReadyForNextRound) } } + func shouldShowSamePageMessage(for player: WKRPlayer) -> Bool { + /* + Conditions: + - player can't be local player + - both players have to be racing + - both players need histories + - both players need at least three pages viewed (don't want to spam messages at start of race) + - last viewed page needs to be the same + - duration must be nil (must not be moving to new page) + - link can't be on the page (don't want players to know they are close) + */ + guard localPlayer != player, + player.state == .racing, + localPlayer.state == .racing, + let playerEntries = player.raceHistory?.entries, + let localEntries = localPlayer.raceHistory?.entries, + playerEntries.count > 2, + localEntries.count > 2, + let localPage = localEntries.last?.page, + playerEntries.last?.page == localPage, + playerEntries.last?.duration == nil, + localEntries.last?.duration == nil, + let isLinkOnPage = activeRace?.attributes(for: localPage).linkOnPage, + !isLinkOnPage else { return false } + + return true + } + // MARK: - Race End - func checkForRaceEnd() { + func calculateSessionPoints() -> [WKRPlayerProfile: Int] { var sessionPoints = [WKRPlayerProfile: Int]() for race in completedRaces { for (player, points) in race.calculatePoints() { @@ -130,6 +175,11 @@ public class WKRGame { } } } + return sessionPoints + } + + func checkForRaceEnd() { + var sessionPoints = calculateSessionPoints() let racePoints = activeRace?.calculatePoints() ?? [:] for (player, points) in racePoints { @@ -140,19 +190,25 @@ public class WKRGame { } } - let currentResults = WKRResultsInfo(players: players, racePoints: racePoints, sessionPoints: sessionPoints) + let currentResults = WKRResultsInfo(racePlayers: activeRace?.players ?? players, + racePoints: racePoints, + sessionPoints: sessionPoints) + guard let race = activeRace, localPlayer.isHost, race.shouldEnd() else { - localResultsUpdated?(currentResults) + listenerUpdate?(.localResults(currentResults)) return } - let adjustedPlayers = players + let adjustedPlayers = activeRace?.players ?? players for player in adjustedPlayers where player.state == .racing { player.state = .forcedEnd } - let results = WKRResultsInfo(players: adjustedPlayers, racePoints: racePoints, sessionPoints: sessionPoints) + let results = WKRResultsInfo(racePlayers: adjustedPlayers, + racePoints: racePoints, + sessionPoints: sessionPoints) + finishedRace() - hostResultsCreated?(results) + listenerUpdate?(.hostResults(results)) } } diff --git a/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift b/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift index 07d5ead..16f9740 100644 --- a/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift +++ b/WKRKit/WKRKit/Game/WKRPreRaceConfig.swift @@ -35,26 +35,30 @@ public struct WKRPreRaceConfig: Codable, Equatable { /// Creates a race config object based on starting page and voting data /// /// - Returns: The new race config - internal func raceConfig() -> WKRRaceConfig? { - guard let finalPage = voteInfo.selectFinalPage() else { - return nil + internal func raceConfig(with weights: [WKRPlayerProfile: Int]) -> (WKRRaceConfig?, WKRLogEvent?) { + let (finalPage, logEvent) = voteInfo.selectFinalPage(with: weights) + guard let page = finalPage else { + return (nil, logEvent) } - return WKRRaceConfig(starting: startingPage, ending: finalPage) + return (WKRRaceConfig(starting: startingPage, ending: page), logEvent) } /// Creates a WKRPreRaceConfig object /// /// - Parameter completionHandler: The handler holding the new config object - static func new(completionHandler: @escaping ((_ config: WKRPreRaceConfig?) -> Void)) { - let finalArticles = WKRKitConstants.current.finalArticles() + static func new(completionHandler: @escaping ((_ config: WKRPreRaceConfig?, _ logEvents: [WKRLogEvent]) -> Void)) { + + let (finalArticles, resetLogEvent) = WKRSeenFinalArticlesStore.unseenArticles() + var logEvents = [resetLogEvent] let operationQueue = OperationQueue() // Get a few more than neccessary random paths in case some final articles are no longer valid var randomPaths = [String]() - while randomPaths.count < Int(Double(WKRKitConstants.current.votingArticlesCount) * 1.5) { - if let randomPath = finalArticles.randomElement, !randomPaths.contains(randomPath) { - randomPaths.append(randomPath) - } + let numberOfPagesToFetch = WKRKitConstants.current.votingArticlesCount + 1 + + // pages are suffled so we can just take index 0-n from the shuffled array + for index in 0.. WKROperation in + let endingPageOperations = randomPaths.map { path -> WKROperation in let operation = WKROperation() operation.addExecutionBlock { [unowned operation] in - WKRPageFetcher.fetch(path: path) { (page) in - if let page = page { + // don't use cache to make sure to get most recent page + WKRPageFetcher.fetch(path: path, useCache: false) { page, isRedirect in + // 1. Make sure not redirect + // 2. Make sure page not nil + // 3. Make sure page not already in voting list for this race + // 4. Make sure page is not a link to a section "/USA#History" + // 5. Sometimes removed pages redirect to the Wikipedia homepage. + // 6/7. Make sure link not equal to starting page + if !isRedirect, + let page = page, + !pages.contains(page), + !page.url.absoluteString.contains("#"), + page.title != "Wikipedia, the free encyclopedia", + let startingPage = startingPage, + startingPage.url != page.url { pages.append(page) + } else { + logEvents.append(WKRLogEvent(type: .votingArticleValidationFailure, + attributes: nil)) } operation.state = .isFinished } } + operation.addDependency(startingPageOperation) completedOperation.addDependency(operation) return operation } - operationQueue.addOperations(operations, waitUntilFinished: false) + operationQueue.addOperations([startingPageOperation, completedOperation], waitUntilFinished: false) + operationQueue.addOperations(endingPageOperations, waitUntilFinished: false) } } diff --git a/WKRKit/WKRKit/Game/WKRRace.swift b/WKRKit/WKRKit/Game/WKRRace.swift index b966619..865ba38 100644 --- a/WKRKit/WKRKit/Game/WKRRace.swift +++ b/WKRKit/WKRKit/Game/WKRRace.swift @@ -37,7 +37,7 @@ internal struct WKRRace { /// /// - Parameter player: The update player internal mutating func playerUpdated(_ player: WKRPlayer) { - if let index = players.index(of: player) { + if let index = players.firstIndex(of: player) { players[index] = player } else { players.append(player) @@ -51,10 +51,20 @@ internal struct WKRRace { /// /// - Parameter page: The page to check againts /// - Returns: Tuple with found page and link on page values. - internal func attributesFor(_ page: WKRPage) -> (foundPage: Bool, linkOnPage: Bool) { + internal func attributes(for page: WKRPage) -> (foundPage: Bool, linkOnPage: Bool) { + var adjustedURL = page.url + + // Adjust for links to sections + if adjustedURL.absoluteString.contains("#") { + var components = adjustedURL.absoluteString.components(separatedBy: "#") + if components.count == 2, let newURL = URL(string: components[0]) { + adjustedURL = newURL + } + } + if page == finalPage { return (true, false) - } else if page.url == finalPage.url { + } else if adjustedURL == finalPage.url { return (true, false) } else if page.title == finalPage.title { return (true, false) @@ -67,7 +77,7 @@ internal struct WKRRace { // MARK: - End Race Helpers /// Calculates how my points each place should receive for the race. Every player that found the article - /// gets points for how many players they did better then. The first player also gets the race bonus points + /// gets points for how many players they did better then. All players also get the race bonus points /// if there are any. /// /// - Returns: Each player's points in a dictionary @@ -82,11 +92,7 @@ internal struct WKRRace { return times[lhs] ?? 0 < times[rhs] ?? 0 } for (index, player) in positions.enumerated() { - if index == 0 { - points[player.profile] = players.count - 1 + bonusPoints - } else { - points[player.profile] = players.count - index - 1 + bonusPoints - } + points[player.profile] = players.count - index - 1 + bonusPoints } return points } diff --git a/WKRKit/WKRKit/Game/WKRReadyStates.swift b/WKRKit/WKRKit/Game/WKRReadyStates.swift index 4917a17..a2ba8e1 100644 --- a/WKRKit/WKRKit/Game/WKRReadyStates.swift +++ b/WKRKit/WKRKit/Game/WKRReadyStates.swift @@ -14,14 +14,15 @@ public struct WKRReadyStates: Codable { self.players = players } - public func playerReady(_ player: WKRPlayer) -> Bool { - guard let index = players.index(of: player) else { return false } + public func isPlayerReady(_ player: WKRPlayer) -> Bool { + guard let index = players.firstIndex(of: player) else { return false } return players[index].state == .readyForNextRound } - var isReadyForNextRound: Bool { - for player in players where player.state != .readyForNextRound { - return false + func areAllRacePlayersReady(racePlayers: [WKRPlayer]) -> Bool { + let relevantPlayers = players.filter({ racePlayers.contains($0) && $0.state != .quit }) + for player in relevantPlayers where player.state != .readyForNextRound { + return false } return true } diff --git a/WKRKit/WKRKit/Game/WKRVoteInfo.swift b/WKRKit/WKRKit/Game/WKRVoteInfo.swift index d563eab..90cd484 100644 --- a/WKRKit/WKRKit/Game/WKRVoteInfo.swift +++ b/WKRKit/WKRKit/Game/WKRVoteInfo.swift @@ -12,7 +12,7 @@ public struct WKRVoteInfo: Codable, Equatable { // MARK: - Properties - private let pages: [WKRPage] + internal let pages: [WKRPage] private var playerVotes = [WKRPlayerProfile: WKRPage]() public var pageCount: Int { @@ -23,7 +23,7 @@ public struct WKRVoteInfo: Codable, Equatable { internal init(pages: [WKRPage]) { let sortedPages = pages.sorted { (pageOne, pageTwo) -> Bool in - return pageOne.title ?? "" < pageTwo.title ?? "" + return pageOne.title?.lowercased() ?? "" < pageTwo.title?.lowercased() ?? "" } self.pages = sortedPages } @@ -34,11 +34,11 @@ public struct WKRVoteInfo: Codable, Equatable { playerVotes[profile] = page } - internal func selectFinalPage() -> WKRPage? { + internal func selectFinalPage(with weights: [WKRPlayerProfile: Int]) -> (WKRPage?, WKRLogEvent?) { var votes = [WKRPage: Int]() pages.forEach { votes[$0] = 0 } - for page in Array(playerVotes.values) { + for page in playerVotes.values { let pageVotes = votes[page] ?? 0 votes[page] = pageVotes + 1 } @@ -55,7 +55,63 @@ public struct WKRVoteInfo: Codable, Equatable { } } - return pagesWithMostVotes.randomElement + var logEvent: WKRLogEvent? + let totalPoints = Double(weights.values.reduce(0, +)) + let lowestScoringPlayers = weights.sorted(by: { $0.value < $1.value }) + + // 1. Make sure there is a tie + // 2. Make sure a few points have been given + // 3. Make sure we have a lowest player + // 4. Make sure player voted + // 5. Make sure player voted for article with most/tied amount of votes + if pagesWithMostVotes.count > 1, + totalPoints > 4, + lowestScoringPlayers.count > 1, + let player = lowestScoringPlayers.first, + let page = playerVotes[player.key], + pagesWithMostVotes.contains(page) { + + /* + P1 Points = player with lowest points + P2 Points = player with next lowest points + Break Tie Chance = chance that p1 will break the tie + Total Chance = Probability p1 article choosen (break tie chance + random chance) + - x way = x number of articles tied with most votes + + +-----------+-----------+------------------+----------------------+----------------------+ + | P1 Points | P2 Points | Break Tie Chance | Total Chance (2 way) | Total Chance (3 way) | + +-----------+-----------+------------------+----------------------+----------------------+ + | 0...4 | 10 | 60% | 80% | 73.2% | + | 5 | 10 | 50% | 75% | 66.5% | + | 6 | 10 | 40% | 70% | 59.8% | + | 7 | 10 | 30% | 65% | 53.1% | + | 8 | 10 | 20% | 60% | 46.4% | + | 9 | 10 | 10% | 55% | 39.7% | + | 10 | 10 | 0% | 50% | 33% | + +-----------+-----------+------------------+----------------------+----------------------+ + */ + let nextLowestPoints = Double(lowestScoringPlayers[1].value) + let inversePercentChance = max(Double(player.value) / nextLowestPoints, 0.4) + + let chance = (1.0 - inversePercentChance) + (1 - (1.0 - inversePercentChance)) / 2 + var attributes: [String: Any] = [ + "TiedCount": pagesWithMostVotes.count, + "Chance": chance, + "RawPointDiff": Int(nextLowestPoints) - player.value + ] + + if Double.random(in: 0..<1) > inversePercentChance { + attributes["BrokeTie"] = 1 + return (page, WKRLogEvent(type: .votingArticlesWeightedTiebreak, + attributes: attributes)) + } else { + attributes["BrokeTie"] = 0 + logEvent = WKRLogEvent(type: .votingArticlesWeightedTiebreak, + attributes: attributes) + } + } + + return (pagesWithMostVotes.randomElement, logEvent) } // MARK: - Public Accessors @@ -70,7 +126,7 @@ public struct WKRVoteInfo: Codable, Equatable { } public func index(of page: WKRPage) -> Int? { - return pages.index(of: page) + return pages.firstIndex(of: page) } } diff --git a/WKRKit/WKRKit/Info.plist b/WKRKit/WKRKit/Info.plist index 5ba2b41..d8e819c 100644 --- a/WKRKit/WKRKit/Info.plist +++ b/WKRKit/WKRKit/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.6 + 3.6.4 CFBundleVersion - 5088 + 8694 NSPrincipalClass diff --git a/WKRKit/WKRKit/Manager/WKRManager+Codable.swift b/WKRKit/WKRKit/Manager/WKRManager+Codable.swift index e6baea1..9015b65 100644 --- a/WKRKit/WKRKit/Manager/WKRManager+Codable.swift +++ b/WKRKit/WKRKit/Manager/WKRManager+Codable.swift @@ -15,20 +15,46 @@ extension WKRGameManager { internal func receivedRaw(_ object: WKRCodable, from player: WKRPlayerProfile) { if let preRaceConfig = object.typeOf(WKRPreRaceConfig.self) { game.preRaceConfig = preRaceConfig - voteInfoUpdate?(preRaceConfig.voteInfo) + votingUpdate(.voteInfo(preRaceConfig.voteInfo)) if webView?.url != preRaceConfig.startingPage.url { webView?.load(URLRequest(url: preRaceConfig.startingPage.url)) } + + WKRSeenFinalArticlesStore.addLocalPlayerSeenFinalPages(preRaceConfig.voteInfo.pages) } else if let raceConfig = object.typeOf(WKRRaceConfig.self) { game.startRace(with: raceConfig) - voteFinalPageUpdate?(raceConfig.endingPage) + votingUpdate(.finalPage(raceConfig.endingPage)) } else if let playerObject = object.typeOf(WKRPlayer.self) { if !game.players.contains(playerObject) && playerObject != localPlayer { peerNetwork.send(object: WKRCodable(localPlayer)) } game.playerUpdated(playerObject) + // if: other player just got to the same page + // else if: local player just got to a new page + var samePagePlayers = [WKRPlayerProfile]() + if playerObject != localPlayer && game.shouldShowSamePageMessage(for: playerObject) { + samePagePlayers.append(playerObject.profile) + } else if playerObject == localPlayer { + for player in game.players where game.shouldShowSamePageMessage(for: player) { + samePagePlayers.append(player.profile) + } + } + + var samePageMessage: String? + if samePagePlayers.count == 1 { + samePageMessage = "\(samePagePlayers[0].name) is on same page" + } else if samePagePlayers.count > 1 { + samePageMessage = "\(samePagePlayers.count) players are on same page" + } + if let message = samePageMessage { + enqueue(message: message, + duration: 2.0, + isRaceSpecific: true, + playHaptic: false) + } + // Player joined mid-session if playerObject.state == .connecting && localPlayer.state != .connecting @@ -49,13 +75,30 @@ extension WKRGameManager { if let gameState = object.typeOfEnum(WKRGameState.self) { transitionGameState(to: gameState) } else if let message = object.typeOfEnum(WKRPlayerMessage.self), player != localPlayer.profile { - enqueue(message: message.text(for: player), duration: 5.0) + var isRaceSpecific = true + var playHaptic = false + if message == .quit { + isRaceSpecific = false + } else if message == .foundPage { + playHaptic = true + } + + // Don't show "on USA" message if expected to show "is on same page" message + let lastPageTitle = localPlayer.raceHistory?.entries.last?.page.title ?? "" + if message == .onUSA && lastPageTitle == "United States" { + return + } + + enqueue(message: message.text(for: player), + duration: 3.0, + isRaceSpecific: isRaceSpecific, + playHaptic: playHaptic) } else if let error = object.typeOfEnum(WKRFatalError.self), !isFailing { isFailing = true localPlayer.state = .quit peerNetwork.send(object: WKRCodable(localPlayer)) peerNetwork.disconnect() - stateUpdate(gameState, error) + gameUpdate(.error(error)) } } @@ -63,42 +106,52 @@ extension WKRGameManager { guard let int = object.typeOf(WKRInt.self) else { fatalError("Object not a WKRInt type") } switch int.type { case .votingTime, .votingPreRaceTime: - voteTimeUpdate?(int.value) + votingUpdate(.remainingTime(int.value)) case .resultsTime: - resultsTimeUpdate?(int.value) + resultsUpdate(.remainingTime(int.value)) case .bonusPoints: let string = int.value == 1 ? "Point" : "Points" - let message = "Match Bonus Now \(int.value) " + string - enqueue(message: message) + let message = "Race Bonus Now \(int.value) " + string + enqueue(message: message, + duration: 2.0, + isRaceSpecific: true, + playHaptic: false) case .showReady: - resultsShowReady?(int.value == 1) + resultsUpdate(.isReadyUpEnabled(int.value == 1)) } } // MARK: - Game Updates private func receivedFinalResults(_ resultsInfo: WKRResultsInfo) { + alertView.clearRaceSpecificMessages() game.finishedRace() if gameState != .hostResults { transitionGameState(to: .hostResults) - - WKRConnectionTester.start(timeout: 15.0, completionHandler: { success in - if !success { - self.errorOccurred(.internetSpeed) - } - }) } hostResultsInfo = resultsInfo - resultsInfoHostUpdate?(resultsInfo) + resultsUpdate(.hostResultsInfo(resultsInfo)) if localPlayer.state == .racing { localPlayer.state = .forcedEnd } if localPlayer.shouldGetPoints { localPlayer.shouldGetPoints = false - pointsUpdate(resultsInfo.raceRewardPoints(for: localPlayer)) + + let points = resultsInfo.raceRewardPoints(for: localPlayer) + var place: Int? + for playerIndex in 0.. WKRPageNavigation { - return WKRPageNavigation(pageURLBlocked: { [weak self] url in - self?.enqueue(message: "Link not allowed", duration: 1.0) - self?.logEvent("pageBlocked", ["PageURL": self?.truncated(url: url) as Any]) - }, pageLoadingError: { [weak self] in - self?.enqueue(message: "Error loading page", duration: 5.0) - self?.webView.completedPageLoad() - }, pageStartedLoading: { [weak self] in - self?.webView.startedPageLoad() - if let player = self?.localPlayer { - player.finishedViewingLastPage() - self?.peerNetwork.send(object: WKRCodable(player)) - } - self?.linkCountUpdate(self?.localPlayer.raceHistory?.entries.count ?? 0) - }, pageLoaded: { [weak self] page in - self?.webView.completedPageLoad() - - var linkHere = false - var foundPage = false - - let lastPageHadLink = self?.localPlayer.raceHistory?.entries.last?.linkHere ?? false - if let attributes = self?.game.activeRace?.attributesFor(page) { - if attributes.foundPage { - foundPage = true - self?.peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.foundPage)) - } else if attributes.linkOnPage { - linkHere = true - if !lastPageHadLink { - self?.peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.linkOnPage)) + func createPageNavigation() { + let pageNavigation = WKRPageNavigation(pageUpdate: { [weak self] pageUpdate in + guard let self = self else { return } + + switch pageUpdate { + case .urlBlocked(let url): + self.displayNetworkAlert(title: "Link not allowed", duration: 2) + self.gameUpdate(.log(WKRLogEvent(type: .pageBlocked, + attributes: [ + "PageURL": self.truncated(url: url) as Any + ]))) + case .loadingError: + self.displayNetworkAlert(title: "Error loading page", duration: 3) + + self.webView?.completedPageLoad() + self.gameUpdate(.log(WKRLogEvent(type: .pageLoadingError, attributes: nil))) + + // use a bit of an extra timeout to give player chance to reconnect + WKRConnectionTester.start(timeout: WKRKitConstants.current.connectionTestTimeout * 3, + completionHandler: { success in + if !success { + self.localErrorOccurred(.internetSpeed) } - } else if lastPageHadLink { - self?.peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.missedLink)) + }) + case .startedLoading: + self.webView?.startedPageLoad() + let pixelsScrolled = self.webView?.pixelsScrolled ?? 0 + self.localPlayer.finishedViewingLastPage(pixelsScrolled: pixelsScrolled) + self.peerNetwork.send(object: WKRCodable(self.localPlayer)) + + let linkCount = self.localPlayer.raceHistory?.entries.count ?? 0 + self.gameUpdate(.playerRaceLinkCountForCurrentRace(linkCount)) + case .loaded(let page): + self.pageLoaded(page) + case .loadingProgess(let progress): + DispatchQueue.main.async { + self.webView?.networkProgress = progress } } + }) + webView?.navigationDelegate = pageNavigation + self.pageNavigation = pageNavigation + } - guard let localPlayer = self?.localPlayer else { - return - } + private func pageLoaded(_ page: WKRPage) { + webView?.completedPageLoad() - localPlayer.nowViewing(page: page, linkHere: linkHere) + var linkHere = false + var foundPage = false + + if page.title == "United States" { + self.peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.onUSA)) + } - if foundPage { - localPlayer.state = .foundPage - self?.transitionGameState(to: .results) + let lastPageHadLink = localPlayer.raceHistory?.entries.last?.linkHere ?? false + if let attributes = game.activeRace?.attributes(for: page) { + if attributes.foundPage { + foundPage = true + peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.foundPage)) + if let time = localPlayer.raceHistory?.duration { + gameUpdate(.log(WKRLogEvent(type: .foundPage, attributes: ["Time": time]))) + } + } else if attributes.linkOnPage { + linkHere = true + if !lastPageHadLink { + peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.linkOnPage)) + gameUpdate(.log(WKRLogEvent(type: .linkOnPage, attributes: nil))) + } + } else if lastPageHadLink { + peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.missedLink)) + gameUpdate(.log(WKRLogEvent(type: .missedLink, attributes: nil))) } + } - self?.peerNetwork.send(object: WKRCodable(localPlayer)) - self?.logEvent("pageView", ["Page": page.title as Any]) - }) + localPlayer.nowViewing(page: page, linkHere: linkHere) + + if foundPage { + localPlayer.state = .foundPage + transitionGameState(to: .results) + } + + peerNetwork.send(object: WKRCodable(localPlayer)) + // capitalized to keep consistent with past analytics + gameUpdate(.log(WKRLogEvent(type: .pageView, attributes: ["Page": page.title?.capitalized as Any]))) + } + + private func displayNetworkAlert(title: String, duration: Double) { + guard gameState != .results || + gameState != .hostResults || + gameState != .points else { + return + } + enqueue(message: title, + duration: duration, + isRaceSpecific: true, + playHaptic: true) } private func truncated(url: URL) -> String { diff --git a/WKRKit/WKRKit/Manager/WKRManager+PeerNetwork.swift b/WKRKit/WKRKit/Manager/WKRManager+PeerNetwork.swift index 6184f0a..7ce2afd 100644 --- a/WKRKit/WKRKit/Manager/WKRManager+PeerNetwork.swift +++ b/WKRKit/WKRKit/Manager/WKRManager+PeerNetwork.swift @@ -13,31 +13,32 @@ extension WKRGameManager { // MARK: - WKRPeerNetwort func configure(network: WKRPeerNetwork) { - network.objectReceived = { [weak self] object, profile in - DispatchQueue.main.async { - if !(self?.isFailing ?? false) { - self?.receivedCodable(object, from: profile) - } - } - } - network.playerConnected = { [weak self] profile in - if let player = self?.localPlayer { - self?.peerNetwork.send(object: WKRCodable(player)) - } - } - network.playerDisconnected = { [weak self] profile in - if profile == self?.localPlayer.profile { - if self?.gameState != .preMatch { - self?.errorOccurred(.noPeers) + network.networkUpdate = { [weak self] networkUpdate in + guard let self = self else { return } + + switch networkUpdate { + case .object(let object, let profile): + DispatchQueue.main.async { + if !self.isFailing { + self.receivedCodable(object, from: profile) + } } - } else { - let disconnectedPlayerIsHost = self?.game.players.first(where: { player -> Bool in - return player.profile == profile - })?.isHost ?? false - if disconnectedPlayerIsHost { - self?.errorOccurred(.disconnected) + case .playerConnected: + self.peerNetwork.send(object: WKRCodable(self.localPlayer)) + case .playerDisconnected(let profile): + if profile == self.localPlayer.profile { + if self.gameState != .preMatch { + self.localErrorOccurred(.noPeers) + } + } else { + let disconnectedPlayerIsHost = self.game.players.first(where: { player -> Bool in + return player.profile == profile + })?.isHost ?? false + if disconnectedPlayerIsHost { + self.localErrorOccurred(.disconnected) + } + self.game.playerDisconnected(profile) } - self?.game.playerDisconnected(profile) } } } diff --git a/WKRKit/WKRKit/Manager/WKRManager.swift b/WKRKit/WKRKit/Manager/WKRManager.swift index 61b45d2..8ff4b71 100644 --- a/WKRKit/WKRKit/Manager/WKRManager.swift +++ b/WKRKit/WKRKit/Manager/WKRManager.swift @@ -10,6 +10,31 @@ import WKRUIKit public class WKRGameManager { + // MARK: Types + + public enum GameUpdate { + case state(WKRGameState) + case error(WKRFatalError) + case log(WKRLogEvent) + + case playerRaceLinkCountForCurrentRace(Int) + case playerStatsForLastRace(points: Int, place: Int?, webViewPixelsScrolled: Int) + } + + public enum VotingUpdate { + case remainingTime(Int) + case voteInfo(WKRVoteInfo) + case finalPage(WKRPage) + } + + public enum ResultsUpdate { + case isReadyUpEnabled(Bool) + case remainingTime(Int) + case resultsInfo(WKRResultsInfo) + case hostResultsInfo(WKRResultsInfo) + case readyStates(WKRReadyStates) + } + // MARK: - Public Getters public var finalPageURL: URL? { @@ -31,91 +56,72 @@ public class WKRGameManager { internal var isFailing = false internal let game: WKRGame - internal let localPlayer: WKRPlayer + public let localPlayer: WKRPlayer internal let peerNetwork: WKRPeerNetwork - internal var pageNavigation: WKRPageNavigation! + internal var pageNavigation: WKRPageNavigation? // MARK: - Closures - internal let stateUpdate: ((WKRGameState, WKRFatalError?) -> Void) - internal let pointsUpdate: ((Int) -> Void) - internal let linkCountUpdate: ((Int) -> Void) - internal let logEvent: ((String, [String: Any]?) -> Void) - - internal var resultsShowReady: ((Bool) -> Void)? - internal var resultsTimeUpdate: ((Int) -> Void)? - internal var resultsInfoHostUpdate: ((WKRResultsInfo) -> Void)? - - internal var voteTimeUpdate: ((Int) -> Void)? - internal var voteInfoUpdate: ((WKRVoteInfo) -> Void)? - internal var voteFinalPageUpdate: ((WKRPage) -> Void)? + internal let gameUpdate: ((GameUpdate) -> Void) + internal let votingUpdate: ((VotingUpdate) -> Void) + internal let resultsUpdate: ((ResultsUpdate) -> Void) // MARK: - User Interface - public weak var webView: WKRUIWebView! { + public weak var webView: WKRUIWebView? { didSet { - pageNavigation = newPageNavigation() - webView.navigationDelegate = pageNavigation + createPageNavigation() } } internal let alertView = WKRUIAlertView() // MARK: - Initialization - public convenience init(networkConfig: WKRPeerNetworkConfig, - stateUpdate: @escaping ((WKRGameState, WKRFatalError?) -> Void), - pointsUpdate: @escaping ((Int) -> Void), - linkCountUpdate: @escaping ((Int) -> Void), - logEvent: @escaping (((String, [String: Any]?)) -> Void)) { - - switch networkConfig { - case .solo(let name): - self.init(soloPlayerName: name, - stateUpdate: stateUpdate, - pointsUpdate: pointsUpdate, - linkCountUpdate: linkCountUpdate, - logEvent: logEvent) - case .multiwindow(let multiWindowName, let isHost): - self.init(multiWindowName: multiWindowName, - isPlayerHost: isHost, - stateUpdate: stateUpdate, - pointsUpdate: pointsUpdate, - linkCountUpdate: linkCountUpdate, - logEvent: logEvent) - case .mpc(let mpcServiceType, let session, let isHost): - self.init(mpcServiceType: mpcServiceType, - session: session, - isPlayerHost: isHost, - stateUpdate: stateUpdate, - pointsUpdate: pointsUpdate, - linkCountUpdate: linkCountUpdate, - logEvent: logEvent) - } - - } - - internal init(player: WKRPlayer, - network: WKRPeerNetwork, - stateUpdate: @escaping ((WKRGameState, WKRFatalError?) -> Void), - pointsUpdate: @escaping ((Int) -> Void), - linkCountUpdate: @escaping ((Int) -> Void), - logEvent: @escaping (String, [String: Any]?) -> Void) { - - self.stateUpdate = stateUpdate - self.pointsUpdate = pointsUpdate - self.linkCountUpdate = linkCountUpdate - self.logEvent = logEvent - - localPlayer = player - peerNetwork = network - - game = WKRGame(localPlayer: localPlayer, isSolo: network is WKRSoloNetwork) - if player.isHost { - configure(game: game) + public init(networkConfig: WKRPeerNetworkConfig, + gameUpdate: @escaping ((GameUpdate) -> Void), + votingUpdate: @escaping ((VotingUpdate) -> Void), + resultsUpdate: @escaping ((ResultsUpdate) -> Void)) { + self.gameUpdate = gameUpdate + self.votingUpdate = votingUpdate + self.resultsUpdate = resultsUpdate + + let setup = networkConfig.create() + localPlayer = setup.player + localPlayer.isCreator = WKRPlayer.isLocalPlayerCreator + peerNetwork = setup.network + + game = WKRGame(localPlayer: localPlayer, isSolo: peerNetwork is WKRSoloNetwork) + game.listenerUpdate = { [weak self] update in + guard let self = self else { return } + switch update { + case .bonusPoints(let points): + guard self.localPlayer.isHost else { return } + let bonusPoints = WKRCodable(int: WKRInt(type: .bonusPoints, value: points)) + self.peerNetwork.send(object: bonusPoints) + case .playersReadyForNextRound: + guard self.localPlayer.isHost, self.gameState == .hostResults else { return } + //swiftlint:disable:next line_length + DispatchQueue.main.asyncAfter(deadline: .now() + WKRRaceDurationConstants.resultsAllReadyDelay, execute: { + self.finishResultsCountdown() + }) + case .readyStates(let states): + resultsUpdate(.readyStates(states)) + case .hostResults(let results): + guard self.localPlayer.isHost else { return } + DispatchQueue.main.async { + let state = WKRGameState.hostResults + self.peerNetwork.send(object: WKRCodable(enum: state)) + self.peerNetwork.send(object: WKRCodable(results)) + self.prepareResultsCountdown() + } + case .localResults(let results): + guard self.gameState == .results || self.gameState == .race else { return } + resultsUpdate(.resultsInfo(results)) + } } + configure(network: peerNetwork) - peerNetwork.send(object: WKRCodable(self.localPlayer)) } @@ -123,57 +129,28 @@ public class WKRGameManager { alertView.removeFromSuperview() } - // MARK: - View Controller Closures - - public func voting(timeUpdate: @escaping ((Int) -> Void), - infoUpdate: @escaping ((WKRVoteInfo) -> Void), - finalPageUpdate: @escaping ((WKRPage) -> Void)) { - voteTimeUpdate = timeUpdate - voteInfoUpdate = infoUpdate - voteFinalPageUpdate = finalPageUpdate - } - - public func results(showReady: @escaping ((Bool) -> Void), - timeUpdate: @escaping ((Int) -> Void), - infoUpdate: @escaping ((WKRResultsInfo) -> Void), - hostInfoUpdate: @escaping ((WKRResultsInfo) -> Void), - readyStatesUpdate: @escaping ((WKRReadyStates) -> Void)) { - - resultsShowReady = showReady - resultsTimeUpdate = timeUpdate - resultsInfoHostUpdate = hostInfoUpdate - - game.readyStatesUpdated = readyStatesUpdate - game.localResultsUpdated = { [weak self] results in - if self?.gameState == .results || self?.gameState == .race { - infoUpdate(results) - } - } - } - // MARK: - User Interface public func hostNetworkInterface() -> UIViewController? { return peerNetwork.hostNetworkInterface() } - public func enqueue(message: String, duration: Double = 5.0) { - alertView.enqueue(text: message, duration: duration) + public func enqueue(message: String, duration: Double, isRaceSpecific: Bool, playHaptic: Bool) { + alertView.enqueue(text: message, + duration: duration, + isRaceSpecific: isRaceSpecific, + playHaptic: playHaptic) } // MARK: - Actions - internal func errorOccurred(_ error: WKRFatalError) { - isFailing = true + internal func localErrorOccurred(_ error: WKRFatalError) { let codable = WKRCodable(enum: error) - receivedEnum(codable, from: localPlayer.profile) if error == .configCreationFailed && localPlayer.isHost { peerNetwork.send(object: codable) + } else { + receivedEnum(codable, from: localPlayer.profile) } - localPlayer.state = .quit - peerNetwork.send(object: WKRCodable(localPlayer)) - stateUpdate(gameState, error) - peerNetwork.disconnect() } public func player(_ action: WKRPlayerAction) { @@ -187,6 +164,7 @@ public class WKRGameManager { case .voted(let page): peerNetwork.send(object: WKRCodable(page)) case .neededHelp: + localPlayer.neededHelp() peerNetwork.send(object: WKRCodable(enum: WKRPlayerMessage.neededHelp)) case .forfeited: localPlayer.state = .forfeited diff --git a/WKRKit/WKRKit/Network/WKRGameKitNetwork.swift b/WKRKit/WKRKit/Network/WKRGameKitNetwork.swift new file mode 100644 index 0000000..60a2706 --- /dev/null +++ b/WKRKit/WKRKit/Network/WKRGameKitNetwork.swift @@ -0,0 +1,90 @@ +// +// WKRGameKitNetwork.swift +// WKRKit +// +// Created by Andrew Finke on 1/25/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import Foundation +import GameKit + +internal class WKRGameKitNetwork: NSObject, GKMatchDelegate, WKRPeerNetwork { + + // MARK: - Closures + + var networkUpdate: ((WKRPeerNetworkUpdate) -> Void)? + + // MARK: - Properties + + private weak var match: GKMatch? + + // MARK: - Initialization + + init(match: GKMatch) { + self.match = match + super.init() + match.delegate = self + } + + // MARK: - WKRNetwork + + func disconnect() { + match?.disconnect() + } + + func send(object: WKRCodable) { + guard let match = match, let data = try? WKRCodable.encoder.encode(object) else { return } + do { + try match.sendData(toAllPlayers: data, with: .reliable) + networkUpdate?(.object(object, profile: GKLocalPlayer.local.wkrProfile())) + } catch { + print(error) + } + } + + internal func hostNetworkInterface() -> UIViewController? { + return nil + } + + // MARK: - MCSessionDelegate + + func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) { + do { + let object = try WKRCodable.decoder.decode(WKRCodable.self, from: data) + networkUpdate?(.object(object, profile: player.wkrProfile())) + } catch { + print(data.description) + } + } + + func match(_ match: GKMatch, player: GKPlayer, didChange state: GKPlayerConnectionState) { + DispatchQueue.main.async { + switch state { + case .connected: self.networkUpdate?(.playerConnected(profile: player.wkrProfile())) + case .disconnected: self.networkUpdate?(.playerDisconnected(profile: player.wkrProfile())) + default: break + } + + // no players left + if match.players.isEmpty { + self.networkUpdate?(.playerDisconnected(profile: GKLocalPlayer.local.wkrProfile())) + } + } + } + +} + +// MARK: - WKRKit Extensions + +extension GKPlayer { + func wkrProfile() -> WKRPlayerProfile { + return WKRPlayerProfile(name: alias, playerID: playerID) + } +} + +extension WKRPlayer { + static var isLocalPlayerCreator: Bool { + return GKLocalPlayer.local.isAuthenticated && GKLocalPlayer.local.alias == "J3D1 WARR10R" + } +} diff --git a/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift b/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift index 73bdc33..a85fda7 100644 --- a/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRMultiWindowNetwork.swift @@ -12,9 +12,7 @@ internal class WKRSplitViewNetwork: WKRPeerNetwork { // MARK: - Closures - var objectReceived: ((WKRCodable, WKRPlayerProfile) -> Void)? - var playerConnected: ((WKRPlayerProfile) -> Void)? - var playerDisconnected: ((WKRPlayerProfile) -> Void)? + var networkUpdate: ((WKRPeerNetworkUpdate) -> Void)? // MARK: - Types @@ -54,18 +52,18 @@ internal class WKRSplitViewNetwork: WKRPeerNetwork { if splitMessage.sender != playerName { if !self.players.contains(messageSender) { self.players.append(messageSender) - self.playerConnected?(messageSender) + self.networkUpdate?(.playerConnected(profile: messageSender)) } do { let object = try WKRCodable.decoder.decode(WKRCodable.self, from: splitMessage.data) - self.objectReceived?(object, messageSender) + self.networkUpdate?(.object(object, profile: messageSender)) } catch { fatalError(splitMessage.description) } } } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.playerConnected?(localPlayer) + self.networkUpdate?(.playerConnected(profile: localPlayer)) } } @@ -78,9 +76,11 @@ internal class WKRSplitViewNetwork: WKRPeerNetwork { let messageSender = WKRPlayerProfile(name: splitMessage.sender, playerID: splitMessage.sender) DispatchQueue.main.asyncAfter(deadline: .now() + (1.5 / Double(arc4random() % 100))) { - NotificationCenter.default.post(name: Notification.Name("Object"), object: splitMessage, userInfo: nil) + NotificationCenter.default.post(name: Notification.Name("Object"), + object: splitMessage, + userInfo: nil) } - objectReceived?(object, messageSender) + networkUpdate?(.object(object, profile: messageSender)) } func disconnect() { @@ -92,28 +92,3 @@ internal class WKRSplitViewNetwork: WKRPeerNetwork { } } - -// MARK: - WKRKit Extensions - -extension WKRGameManager { - - internal convenience init(multiWindowName: String, - isPlayerHost: Bool, - stateUpdate: @escaping ((WKRGameState, WKRFatalError?) -> Void), - pointsUpdate: @escaping ((Int) -> Void), - linkCountUpdate: @escaping ((Int) -> Void), - logEvent: @escaping (((String, [String: Any]?)) -> Void)) { - - let profile = WKRPlayerProfile(name: multiWindowName, playerID: multiWindowName) - let player = WKRPlayer(profile: profile, isHost: isPlayerHost) - let network = WKRSplitViewNetwork(playerName: multiWindowName, isHost: isPlayerHost) - - self.init(player: player, - network: network, - stateUpdate: stateUpdate, - pointsUpdate: pointsUpdate, - linkCountUpdate: linkCountUpdate, - logEvent: logEvent) - } - -} diff --git a/WKRKit/WKRKit/Network/WKRMultipeerNetwork.swift b/WKRKit/WKRKit/Network/WKRMultipeerNetwork.swift index f138828..33f221d 100644 --- a/WKRKit/WKRKit/Network/WKRMultipeerNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRMultipeerNetwork.swift @@ -13,9 +13,7 @@ internal class WKRMultipeerNetwork: NSObject, MCSessionDelegate, MCBrowserViewCo // MARK: - Closures - var objectReceived: ((WKRCodable, WKRPlayerProfile) -> Void)? - var playerConnected: ((WKRPlayerProfile) -> Void)? - var playerDisconnected: ((WKRPlayerProfile) -> Void)? + var networkUpdate: ((WKRPeerNetworkUpdate) -> Void)? // MARK: - Properties @@ -41,7 +39,7 @@ internal class WKRMultipeerNetwork: NSObject, MCSessionDelegate, MCBrowserViewCo guard let session = session, let data = try? WKRCodable.encoder.encode(object) else { return } do { try session.send(data, toPeers: session.connectedPeers, with: .reliable) - objectReceived?(object, WKRPlayerProfile(peerID: session.myPeerID)) + networkUpdate?(.object(object, profile: session.myPeerID.wkrProfile())) } catch { print(error) } @@ -60,7 +58,7 @@ internal class WKRMultipeerNetwork: NSObject, MCSessionDelegate, MCBrowserViewCo open func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { do { let object = try WKRCodable.decoder.decode(WKRCodable.self, from: data) - objectReceived?(object, WKRPlayerProfile(peerID: peerID)) + networkUpdate?(.object(object, profile: peerID.wkrProfile())) } catch { print(data.description) } @@ -69,13 +67,14 @@ internal class WKRMultipeerNetwork: NSObject, MCSessionDelegate, MCBrowserViewCo open func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { DispatchQueue.main.async { switch state { - case .connected: self.playerConnected?(WKRPlayerProfile(peerID: peerID)) - case .notConnected: self.playerDisconnected?(WKRPlayerProfile(peerID: peerID)) + case .connected: self.networkUpdate?(.playerConnected(profile: peerID.wkrProfile())) + case .notConnected: self.networkUpdate?(.playerDisconnected(profile: peerID.wkrProfile())) default: break } + // no players left if session.connectedPeers.isEmpty { - self.playerDisconnected?(WKRPlayerProfile(peerID: session.myPeerID)) + self.networkUpdate?(.playerDisconnected(profile: session.myPeerID.wkrProfile())) } } } @@ -115,32 +114,8 @@ internal class WKRMultipeerNetwork: NSObject, MCSessionDelegate, MCBrowserViewCo // MARK: - WKRKit Extensions -extension WKRGameManager { - - internal convenience init(mpcServiceType: String, - session: MCSession, - isPlayerHost: Bool, - stateUpdate: @escaping ((WKRGameState, WKRFatalError?) -> Void), - pointsUpdate: @escaping ((Int) -> Void), - linkCountUpdate: @escaping ((Int) -> Void), - logEvent: @escaping (((String, [String: Any]?)) -> Void)) { - - let player = WKRPlayer(profile: WKRPlayerProfile(peerID: session.myPeerID), isHost: isPlayerHost) - let network = WKRMultipeerNetwork(serviceType: mpcServiceType, session: session) - - self.init(player: player, - network: network, - stateUpdate: stateUpdate, - pointsUpdate: pointsUpdate, - linkCountUpdate: linkCountUpdate, - logEvent: logEvent) - } - -} - -extension WKRPlayerProfile { - init(peerID: MCPeerID) { - name = peerID.displayName - playerID = peerID.hashValue.description +extension MCPeerID { + func wkrProfile() -> WKRPlayerProfile { + return WKRPlayerProfile(name: displayName, playerID: hashValue.description) } } diff --git a/WKRKit/WKRKit/Network/WKRPeerNetwork.swift b/WKRKit/WKRKit/Network/WKRPeerNetwork.swift index 3fafc84..922815a 100644 --- a/WKRKit/WKRKit/Network/WKRPeerNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRPeerNetwork.swift @@ -9,11 +9,15 @@ import Foundation internal protocol WKRPeerNetwork: class { - var objectReceived: ((WKRCodable, WKRPlayerProfile) -> Void)? { get set } - var playerConnected: ((WKRPlayerProfile) -> Void)? { get set } - var playerDisconnected: ((WKRPlayerProfile) -> Void)? { get set } + var networkUpdate: ((WKRPeerNetworkUpdate) -> Void)? { get set } func disconnect() func send(object: WKRCodable) func hostNetworkInterface() -> UIViewController? } + +enum WKRPeerNetworkUpdate { + case object(_ object: WKRCodable, profile: WKRPlayerProfile) + case playerConnected(profile: WKRPlayerProfile) + case playerDisconnected(profile: WKRPlayerProfile) +} diff --git a/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift b/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift index 33a09b6..86eb796 100644 --- a/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift +++ b/WKRKit/WKRKit/Network/WKRPeerNetworkConfig.swift @@ -8,21 +8,44 @@ import Foundation import MultipeerConnectivity +import GameKit public enum WKRPeerNetworkConfig { case solo(name: String) - case multiwindow(multiWindowName: String, isHost: Bool) - case mpc(mpcServiceType: String, session: MCSession, isHost: Bool) + case gameKit(match: GKMatch, isHost: Bool) + case multiwindow(windowName: String, isHost: Bool) + case mpc(serviceType: String, session: MCSession, isHost: Bool) public var isHost: Bool { switch self { case .solo: return true - case .multiwindow(_, let isHost): + case .gameKit(_, let isHost): return isHost case .mpc(_, _, let isHost): return isHost + case .multiwindow(_, let isHost): + return isHost + } + } + + func create() -> (player: WKRPlayer, network: WKRPeerNetwork) { + switch self { + case .solo(let name): + let profile = WKRPlayerProfile(name: name, playerID: name) + let player = WKRPlayer(profile: profile, isHost: true) + return (player, WKRSoloNetwork(profile: profile)) + case .gameKit(let match, let isHost): + let player = WKRPlayer(profile: GKLocalPlayer.local.wkrProfile(), isHost: isHost) + return (player, WKRGameKitNetwork(match: match)) + case .mpc(let serviceType, let session, let isHost): + let player = WKRPlayer(profile: session.myPeerID.wkrProfile(), isHost: isHost) + return (player, WKRMultipeerNetwork(serviceType: serviceType, session: session)) + case .multiwindow(let windowName, let isHost): + let profile = WKRPlayerProfile(name: windowName, playerID: windowName) + let player = WKRPlayer(profile: profile, isHost: isHost) + return(player, WKRSplitViewNetwork(playerName: windowName, isHost: isHost)) } } diff --git a/WKRKit/WKRKit/Network/WKRSoloNetwork.swift b/WKRKit/WKRKit/Network/WKRSoloNetwork.swift index 45925c4..d0e76b8 100644 --- a/WKRKit/WKRKit/Network/WKRSoloNetwork.swift +++ b/WKRKit/WKRKit/Network/WKRSoloNetwork.swift @@ -12,9 +12,7 @@ internal class WKRSoloNetwork: WKRPeerNetwork { // MARK: - Closures - var objectReceived: ((WKRCodable, WKRPlayerProfile) -> Void)? - var playerConnected: ((WKRPlayerProfile) -> Void)? - var playerDisconnected: ((WKRPlayerProfile) -> Void)? + var networkUpdate: ((WKRPeerNetworkUpdate) -> Void)? // MARK: - Types @@ -29,7 +27,7 @@ internal class WKRSoloNetwork: WKRPeerNetwork { // MARK: - WKRNetwork func send(object: WKRCodable) { - objectReceived?(object, playerProfile) + networkUpdate?(.object(object, profile: playerProfile)) } func disconnect() { @@ -40,27 +38,3 @@ internal class WKRSoloNetwork: WKRPeerNetwork { } } - -// MARK: - WKRKit Extensions - -extension WKRGameManager { - - internal convenience init(soloPlayerName: String, - stateUpdate: @escaping ((WKRGameState, WKRFatalError?) -> Void), - pointsUpdate: @escaping ((Int) -> Void), - linkCountUpdate: @escaping ((Int) -> Void), - logEvent: @escaping (((String, [String: Any]?)) -> Void)) { - - let profile = WKRPlayerProfile(name: soloPlayerName, playerID: soloPlayerName) - let player = WKRPlayer(profile: profile, isHost: true) - let network = WKRSoloNetwork(profile: profile) - - self.init(player: player, - network: network, - stateUpdate: stateUpdate, - pointsUpdate: pointsUpdate, - linkCountUpdate: linkCountUpdate, - logEvent: logEvent) - } - -} diff --git a/WKRKit/WKRKit/Other/WKRDurationFormatter.swift b/WKRKit/WKRKit/Other/WKRDurationFormatter.swift new file mode 100644 index 0000000..ab063e2 --- /dev/null +++ b/WKRKit/WKRKit/Other/WKRDurationFormatter.swift @@ -0,0 +1,33 @@ +// +// WKRDurationFormatter.swift +// WikiRaces +// +// Created by Andrew Finke on 8/5/17. +// Copyright © 2017 Andrew Finke. All rights reserved. +// + +import Foundation + +public struct WKRDurationFormatter { + private static let maxSeconds: Int = 360 + + public static func string(for duration: Int?, extended: Bool = false) -> String? { + guard let duration = duration else { return nil } + if duration > maxSeconds { + let suffix = extended ? " Min" : " M" + let time = duration / 60 + return time.description + suffix + } else { + let suffix = extended ? " Sec" : " S" + return duration.description + suffix + } + } + + public static func resultsString(for duration: Int?) -> String? { + guard let duration = duration else { return nil } + let minutes = duration / 60 + let seconds = duration % 60 + let secondsString = (seconds < 10 ? "0" : "") + seconds.description + return minutes.description + ":" + secondsString + } +} diff --git a/WKRKit/WKRKit/Other/WKRLogEvent.swift b/WKRKit/WKRKit/Other/WKRLogEvent.swift new file mode 100644 index 0000000..223c190 --- /dev/null +++ b/WKRKit/WKRKit/Other/WKRLogEvent.swift @@ -0,0 +1,21 @@ +// +// WKRLogEvent.swift +// WKRKit +// +// Created by Andrew Finke on 2/27/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import Foundation + +public struct WKRLogEvent { + + public enum EventType: String { + case linkOnPage, missedLink, foundPage, pageBlocked, pageLoadingError, pageView + case collectiveVotingArticlesSeen, localVotingArticlesSeen, localVotingArticlesReset + case votingArticleValidationFailure, votingArticlesWeightedTiebreak + } + + public let type: EventType + public let attributes: [String: Any]? +} diff --git a/WKRKit/WKRKit/Pages/WKRHistory.swift b/WKRKit/WKRKit/Pages/WKRHistory.swift index a17b077..ef444cf 100644 --- a/WKRKit/WKRKit/Pages/WKRHistory.swift +++ b/WKRKit/WKRKit/Pages/WKRHistory.swift @@ -15,6 +15,11 @@ public struct WKRHistory: Codable, Equatable { /// The time the player opened the last page private var lastPageOpenTime: Date + + var timeSinceLastPageOpenTime: Int { + return Int(-lastPageOpenTime.timeIntervalSinceNow) + } + /// The history entries public private(set) var entries = [WKRHistoryEntry]() /// The total time the player has been racing (not including page load times) @@ -47,7 +52,7 @@ public struct WKRHistory: Codable, Equatable { mutating func finishedViewingLastPage() { guard var entry = entries.last else { fatalError("Entries is empty") } - entry.duration = Int(-lastPageOpenTime.timeIntervalSinceNow) + entry.duration = timeSinceLastPageOpenTime entries[entries.count - 1] = entry } diff --git a/WKRKit/WKRKit/Pages/WKRPage.swift b/WKRKit/WKRKit/Pages/WKRPage.swift index d1b59a7..a29948e 100644 --- a/WKRKit/WKRKit/Pages/WKRPage.swift +++ b/WKRKit/WKRKit/Pages/WKRPage.swift @@ -16,7 +16,7 @@ public struct WKRPage: Codable, Hashable, Equatable { // The title of the page public let title: String? // The url of the page - internal let url: URL + public let url: URL // MARK: - Initialization @@ -36,15 +36,10 @@ public struct WKRPage: Codable, Hashable, Equatable { private static func formattedTitle(for title: String?) -> String? { guard let title = title else { return nil } - func smartCapitalize(_ title: String) -> String { - // I can't stand Iphone / Os X - if title.first == "i" { - return title - } else if title.contains("OS X") || title.contains("R2-D2") || title.contains("C-3PO") { - return title - } else { - return title.capitalized - } + func smartFormat(_ title: String) -> String { + return title.replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "'", with: "'") + .replacingOccurrences(of: """, with: "\"") } // charactersToRemove is a fallback if simply replacing the "Wikipedia - " fails one day. @@ -54,18 +49,12 @@ public struct WKRPage: Codable, Hashable, Equatable { // is updated one day to use raw character replacment instead of a string. let index = title.index(title.endIndex, offsetBy: -charactersToRemove) let clippedTitle = title[.. Void)) { + static func fetch(path: String, useCache: Bool, completionHandler: @escaping ((_ page: WKRPage?, _ isRedirect: Bool) -> Void)) { guard let url = URL(string: WKRKitConstants.current.baseURLString + path) else { - completionHandler(nil) + completionHandler(nil, false) return } - fetch(url: url, completionHandler: completionHandler) + fetch(url: url, useCache: useCache, completionHandler: completionHandler) } /// Fetches a random Wikipedia page @@ -50,30 +61,56 @@ internal struct WKRPageFetcher { completionHandler(nil) return } - fetch(url: url, completionHandler: completionHandler) + fetch(url: url, useCache: true) { page, _ in + completionHandler(page) + } } /// Fetches a Wikipedia page at a given url - static func fetch(url: URL, completionHandler: @escaping ((_ page: WKRPage?) -> Void)) { - let task = WKRPageFetcher.session.dataTask(with: url) { (data, response, _) in + static func fetch(url: URL, useCache: Bool, completionHandler: @escaping ((_ page: WKRPage?, _ isRedirect: Bool) -> Void)) { + let session: URLSession + if useCache { + session = WKRPageFetcher.session + } else { + session = WKRPageFetcher.noCacheSession + } + let task = session.dataTask(with: url) { (data, response, _) in if let data = data, let string = String(data: data, encoding: .utf8), let responseUrl = response?.url { - completionHandler(WKRPage(title: title(from: string), url: responseUrl)) + let page = WKRPage(title: title(from: string), url: responseUrl) + let isRedirect = string.contains("Redirected from") + completionHandler(page, isRedirect) } else { - completionHandler(nil) + completionHandler(nil, false) } } task.resume() } /// Fetches a Wikipedia page source. - static func fetchSource(url: URL, completionHandler: @escaping (_ source: String?) -> Void) { - let task = WKRPageFetcher.session.dataTask(with: url) { (data, _, _) in + static func fetchSource(url: URL, + useCache: Bool, + progressHandler: @escaping (_ progress: Float) -> Void, + completionHandler: @escaping (_ source: String?, _ error: Error?) -> Void) { + let session: URLSession + if useCache { + session = WKRPageFetcher.session + } else { + session = WKRPageFetcher.noCacheSession + } + + let taskUUID = UUID() + let task = session.dataTask(with: url) { (data, _, error) in + observations[taskUUID] = nil if let data = data, let string = String(data: data, encoding: .utf8) { - completionHandler(string) + completionHandler(string, nil) } else { - completionHandler(nil) + completionHandler(nil, error) } } + observations[taskUUID] = task.progress.observe(\.fractionCompleted) { progress, _ in + progressHandler(Float(progress.fractionCompleted)) + } + task.resume() } diff --git a/WKRKit/WKRKit/Pages/WKRSeenFinalArticlesStore.swift b/WKRKit/WKRKit/Pages/WKRSeenFinalArticlesStore.swift new file mode 100644 index 0000000..6d8b57c --- /dev/null +++ b/WKRKit/WKRKit/Pages/WKRSeenFinalArticlesStore.swift @@ -0,0 +1,123 @@ +// +// WKRSeenFinalArticlesStore.swift +// WKRKit +// +// Created by Andrew Finke on 1/30/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import Foundation + +public struct WKRSeenFinalArticlesStore { + + // MARK: - Types + + private struct RemoteTransfer: Codable { + internal let articles: [String] + } + + // MARK: - Properties + + private static let defaults = UserDefaults.standard + private static let localPlayersSeenFinalArticlesKey = "WKRKit-LocalPlayerSeenFinalArticles" + private static var localPlayersSeenFinalArticles: [String] { + get { + return defaults.stringArray(forKey: localPlayersSeenFinalArticlesKey) ?? [] + } + set { + defaults.setValue(newValue, forKey: localPlayersSeenFinalArticlesKey) + } + } + private static var uniqueRemotePlayersSeenFinalArticles = Set() + + // MARK: - Helpers + + internal static func unseenArticles() -> (articles: [String], log: WKRLogEvent?) { + var finalArticles = Set(WKRKitConstants.current.finalArticles) + + // the remaining articles could all be invalid (i.e. redirects, deleted pages, etc.) + // make sure that we reset before the rest are invalid. minCount is that buffer. + let minCount = 500 + var resetLog: WKRLogEvent? + + // make sure at least minCount unseen articles left before removing locally seen + if localPlayersSeenFinalArticles.count < finalArticles.count - minCount { + // remove local seen articles from final list + finalArticles = finalArticles.subtracting(localPlayersSeenFinalArticles) + } else { + // player has seen almost all articles already + resetLog = WKRLogEvent(type: .localVotingArticlesReset, + attributes: ["ArticleCount": localPlayersSeenFinalArticles.count]) + resetLocalPlayerSeenFinalArticles() + } + + // make sure at least minCount unseen articles left before removing remotely seen + if uniqueRemotePlayersSeenFinalArticles.count < finalArticles.count - minCount { + finalArticles = finalArticles.subtracting(uniqueRemotePlayersSeenFinalArticles) + } + + return (Array(finalArticles), resetLog) + } + + // MARK: - Local Player + + public static func encodedLocalPlayerSeenFinalArticles() -> Data? { + let object = RemoteTransfer(articles: localPlayersSeenFinalArticles) + return try? JSONEncoder().encode(object) + } + + internal static func addLocalPlayerSeenFinalPages(_ newSeenFinalPages: [WKRPage]) { + let paths = newSeenFinalPages.map({ "/" + $0.url.lastPathComponent }) + var articles = localPlayersSeenFinalArticles + articles.append(contentsOf: paths) + localPlayersSeenFinalArticles = Array(Set(articles)) + } + + private static func resetLocalPlayerSeenFinalArticles() { + localPlayersSeenFinalArticles = [] + } + + // MARK: - Remote Players + + public static func addRemoteTransferData(_ data: Data) { + guard let tranfer = try? JSONDecoder().decode(RemoteTransfer.self, from: data) else { return } + + // 1. Add new paths + // 2. Remove copies from remote array that are already in local array + uniqueRemotePlayersSeenFinalArticles = uniqueRemotePlayersSeenFinalArticles + .union(tranfer.articles) + .subtracting(localPlayersSeenFinalArticles) + } + + public static func isRemoteTransferData(_ data: Data) -> Bool { + guard let _ = try? JSONDecoder().decode(RemoteTransfer.self, from: data) else { return false } + return true + } + + public static func resetRemotePlayersSeenFinalArticles() { + uniqueRemotePlayersSeenFinalArticles = [] + } + + public static func hostLogEvents() -> [WKRLogEvent] { + let seenLocalCount = localPlayersSeenFinalArticles.count + let seenCollectiveCount = Set(localPlayersSeenFinalArticles) + .union(uniqueRemotePlayersSeenFinalArticles) + .count + + return [ + WKRLogEvent(type: .localVotingArticlesSeen, + attributes: ["ArticleCount": seenLocalCount]), + WKRLogEvent(type: .collectiveVotingArticlesSeen, + attributes: ["ArticleCount": seenCollectiveCount]) + ] + } + + public static func localLogEvents() -> [WKRLogEvent] { + let seenLocalCount = localPlayersSeenFinalArticles.count + return [ + WKRLogEvent(type: .localVotingArticlesSeen, + attributes: ["ArticleCount": seenLocalCount]) + ] + } + +} diff --git a/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift b/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift index add960b..d7475d4 100644 --- a/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift +++ b/WKRKit/WKRKit/Players/Container/WKRResultsInfo.swift @@ -33,18 +33,18 @@ public struct WKRResultsInfo: Codable { // MARK: - Initialization - init(players: [WKRPlayer], + init(racePlayers: [WKRPlayer], racePoints: [WKRPlayerProfile: Int], sessionPoints: [WKRPlayerProfile: Int]) { self.racePoints = racePoints + let players = racePlayers.sorted(by: { (lhs, rhs) -> Bool in + return lhs.profile.name.lowercased() < rhs.profile.name.lowercased() + }) - // remove players that weren't in race - let playerProfiles = players.map { $0.profile } + let playerProfiles = racePlayers.map { $0.profile } self.sessionPoints = sessionPoints.filter { playerProfiles.contains($0.key) } self.playersSortedByPoints = players.sorted(by: { (lhs, rhs) -> Bool in - return lhs.profile.name < rhs.profile.name - }).sorted(by: { (lhs, rhs) -> Bool in return sessionPoints[lhs.profile] ?? 0 > sessionPoints[rhs.profile] ?? 0 }) @@ -83,12 +83,6 @@ public struct WKRResultsInfo: Codable { return lhs.profile.name < rhs.profile.name }) - // The players that quit - let quitPlayers: [WKRPlayer] = players.filter({ $0.state == .quit }) - .sorted(by: { (lhs, rhs) -> Bool in - return lhs.profile.name < rhs.profile.name - }) - // The players still racing let racingPlayers: [WKRPlayer] = players.filter({ $0.state == .racing }) .sorted(by: { (lhs, rhs) -> Bool in @@ -101,14 +95,18 @@ public struct WKRResultsInfo: Codable { return lhs.profile.name < rhs.profile.name }) - // Figure out what the difference between disconnected and quit should be + // The players that quit + let quitPlayers: [WKRPlayer] = players.filter({ $0.state == .quit }) + .sorted(by: { (lhs, rhs) -> Bool in + return lhs.profile.name < rhs.profile.name + }) let sortedPlayers: [WKRPlayer] = foundPagePlayers + forcedFinishPlayers + forfeitedPlayers - + quitPlayers + racingPlayers + connectingPlayers + + quitPlayers let otherPlayers: [WKRPlayer] = players.filter { !sortedPlayers.contains($0) } return sortedPlayers + otherPlayers @@ -122,13 +120,12 @@ public struct WKRResultsInfo: Codable { // used to update history controller cells public func updatedPlayer(for player: WKRPlayer) -> WKRPlayer? { - guard let updatedPlayerIndex = playersSortedByState.index(of: player) else { return nil } + guard let updatedPlayerIndex = playersSortedByState.firstIndex(of: player) else { return nil } return playersSortedByState[updatedPlayerIndex] } - public func raceResults(at index: Int) -> (player: WKRPlayer, playerState: WKRPlayerState) { - let player = playersSortedByState[index] - return (player, player.state) + public func raceRankingsPlayer(at index: Int) -> WKRPlayer { + return playersSortedByState[index] } public func sessionResults(at index: Int) -> WKRProfileSessionResults { diff --git a/WKRKit/WKRKit/Players/Single/WKRPlayer.swift b/WKRKit/WKRKit/Players/Single/WKRPlayer.swift index 36dc349..79897d6 100644 --- a/WKRKit/WKRKit/Players/Single/WKRPlayer.swift +++ b/WKRKit/WKRKit/Players/Single/WKRPlayer.swift @@ -10,19 +10,28 @@ import Foundation public class WKRPlayer: Codable, Hashable { - // MARK: - Properties + // MARK: - Properties [Must not break] internal let isHost: Bool + + // poorly named (needs to stay backwards compatible), indicating if time spent + points awarded for race internal var shouldGetPoints = false - public var raceHistory: WKRHistory? - public var state: WKRPlayerState = .connecting + public internal(set) var raceHistory: WKRHistory? + public internal(set) var state: WKRPlayerState = .connecting internal let profile: WKRPlayerProfile public var name: String { return profile.name } + // MARK: - Stat Properties [Optional to be backwards compatible] + + public private(set) var stats: WKRPlayerRaceStats? + internal private(set) var neededHelpCount: Int? + internal private(set) var pixelsScrolledDuringCurrentRace: Int? + public var isCreator: Bool? + // MARK: - Initialization init(profile: WKRPlayerProfile, isHost: Bool) { @@ -35,20 +44,30 @@ public class WKRPlayer: Codable, Hashable { func startedNewRace(on page: WKRPage) { state = .racing raceHistory = WKRHistory(firstPage: page) + neededHelpCount = 0 + pixelsScrolledDuringCurrentRace = 0 + stats = WKRPlayerRaceStats(player: self) } func nowViewing(page: WKRPage, linkHere: Bool) { raceHistory?.append(page, linkHere: linkHere) } - func finishedViewingLastPage() { + func finishedViewingLastPage(pixelsScrolled: Int) { raceHistory?.finishedViewingLastPage() + pixelsScrolledDuringCurrentRace = pixelsScrolled + stats = WKRPlayerRaceStats(player: self) + } + + func neededHelp() { + neededHelpCount = (neededHelpCount ?? 0) + 1 + stats = WKRPlayerRaceStats(player: self) } // MARK: - Hashable - public var hashValue: Int { - return profile.playerID.hashValue + public func hash(into hasher: inout Hasher) { + return profile.playerID.hash(into: &hasher) } //swiftlint:disable:next operator_whitespace diff --git a/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift b/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift index 267a1d8..dced773 100644 --- a/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift +++ b/WKRKit/WKRKit/Players/Single/WKRPlayerMessage.swift @@ -16,6 +16,7 @@ public enum WKRPlayerMessage: Int { case neededHelp case forfeited case quit + case onUSA func text(for player: WKRPlayerProfile) -> String { switch self { @@ -25,6 +26,7 @@ public enum WKRPlayerMessage: Int { case .forfeited: return player.name + " forfeited" case .quit: return player.name + " quit" case .missedLink: return player.name + " missed the link" + case .onUSA: return player.name + " is on USA" } } diff --git a/WKRKit/WKRKit/Players/Single/WKRPlayerRaceStats.swift b/WKRKit/WKRKit/Players/Single/WKRPlayerRaceStats.swift new file mode 100644 index 0000000..500a53c --- /dev/null +++ b/WKRKit/WKRKit/Players/Single/WKRPlayerRaceStats.swift @@ -0,0 +1,73 @@ +// +// WKRPlayerRaceStats.swift +// WKRKit +// +// Created by Andrew Finke on 2/27/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import Foundation + +public struct WKRPlayerRaceStats: Codable, Equatable { + + // MARK: - Properties + + static let pixelFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() + + private var statsDictionary = [String: String]() + + public var raw: [(key: String, value: String)] { + return statsDictionary + .map({ ($0.key, $0.value) }) + .sorted(by: { $0.0 < $1.0 }) + } + + init(player: WKRPlayer) { + statsDictionary["Average time per page"] = avergeTimeSpent(player) + statsDictionary["Distance scrolled"] = "0 Pixels" + if let pixelsScrolled = player.pixelsScrolledDuringCurrentRace, + let formatted = WKRPlayerRaceStats.pixelFormatter.string(from: NSNumber(value: pixelsScrolled)) { + statsDictionary["Distance scrolled"] = formatted + " Pixels" + } + } + + func linksMissed(_ player: WKRPlayer) -> String { + guard let history = player.raceHistory else { return "-" } + let state = player.state + + var linkCount = history.entries.filter({ $0.linkHere }).count + // on a page with the link + if let lastLinkHere = history.entries.last?.linkHere, + lastLinkHere, + state == .racing { + linkCount -= 1 + } + // finished race, link on second to last page (last page is the destination) + if state == .foundPage, + history.entries.count > 2, + history.entries[history.entries.count - 2].linkHere { + linkCount -= 1 + } + return linkCount.description + } + + func avergeTimeSpent(_ player: WKRPlayer) -> String { + guard let history = player.raceHistory, + let duration = history.duration else { return "-" } + var entriesCount = history.entries.count + + // viewing this page, don't count yet + if history.entries.last?.duration == nil { + entriesCount -= 1 + } + if entriesCount == 0 { + return "-" + } + let average = Int(round(Double(duration) / Double(entriesCount))) + return WKRDurationFormatter.string(for: average) ?? "-" + } +} diff --git a/WKRKit/WKRKit/Web Logic/WKRConnectionTester.swift b/WKRKit/WKRKit/Web Logic/WKRConnectionTester.swift index 5c36604..7b4d7af 100644 --- a/WKRKit/WKRKit/Web Logic/WKRConnectionTester.swift +++ b/WKRKit/WKRKit/Web Logic/WKRConnectionTester.swift @@ -26,7 +26,7 @@ public struct WKRConnectionTester { completionHandler(false) } - WKRPageFetcher.fetch(path: "/United_States") { (page) in + WKRPageFetcher.fetch(path: "/United_States", useCache: false) { page, _ in timer.invalidate() if timedOut { // Timer fired, completion handler already called diff --git a/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift b/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift index 3d4c8ec..cfe154f 100644 --- a/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift +++ b/WKRKit/WKRKit/Web Logic/WKRLinkedPagesFetcher.swift @@ -92,12 +92,6 @@ internal class WKRLinkedPagesFetcher: NSObject, WKScriptMessageHandler { webView?.stopLoading() } - func getHintPage(completionHandler: @escaping ((_ page: WKRPage?) -> Void)) { - guard hintIndex < foundURLs.count else { return } - WKRPageFetcher.fetch(url: foundURLs[hintIndex], completionHandler: completionHandler) - hintIndex += 1 - } - // MARK: - Message Actions private func found(linkedPage: String) { diff --git a/WKRKit/WKRKit/Web Logic/WKRPageNavigation.swift b/WKRKit/WKRKit/Web Logic/WKRPageNavigation.swift index 8b90af5..b59d44a 100644 --- a/WKRKit/WKRKit/Web Logic/WKRPageNavigation.swift +++ b/WKRKit/WKRKit/Web Logic/WKRPageNavigation.swift @@ -12,34 +12,31 @@ import WebKit /// A WKNavigationDelegate for controlling Wikipedia page loads during the race internal class WKRPageNavigation: NSObject, WKNavigationDelegate { + // MARK: - Types + + enum PageUpdate { + /// Called when the user taps a banned link (i.e. an image) + case urlBlocked(URL) + /// Called when there is an issue loading the page + case loadingError(Error?) + /// Called when the page starts to load + case startedLoading + /// Called when the page has completed loading + case loaded(WKRPage) + + /// Called when the page is being loaded + case loadingProgess(Float) + } + // MARK: - Properties - /// Called when the user taps a banned link (i.e. an image) - let pageURLBlocked: ((URL) -> Void) - /// Called when there is an issue loading the page - let pageLoadingError: (() -> Void) - /// Called when the page starts to load - let pageStartedLoading: (() -> Void) - /// Called when the page has completed loading - let pageLoaded: ((WKRPage) -> Void) + internal let pageUpdate: ((PageUpdate) -> Void) // MARK: - Initialization /// Creates a new WKRPageNavigation object - /// - /// - Parameters: - /// - pageURLBlocked: Called when the user taps a banned link (i.e. an image) - /// - pageLoadingError: Called when there is an issue loading the page - /// - pageStartedLoading: Called when the page starts to load - /// - pageLoaded: Called when the page has completed loading - init(pageURLBlocked: @escaping ((URL) -> Void), - pageLoadingError: @escaping (() -> Void), - pageStartedLoading: @escaping (() -> Void), - pageLoaded: @escaping ((WKRPage) -> Void)) { - self.pageURLBlocked = pageURLBlocked - self.pageLoadingError = pageLoadingError - self.pageStartedLoading = pageStartedLoading - self.pageLoaded = pageLoaded + init(pageUpdate: @escaping ((PageUpdate) -> Void)) { + self.pageUpdate = pageUpdate } // MARK: - Helpers @@ -78,7 +75,7 @@ internal class WKRPageNavigation: NSObject, WKNavigationDelegate { // Make sure the url is legal for the race guard allow(url: requestURL) else { decisionHandler(.cancel) - pageURLBlocked(requestURL) + pageUpdate(.urlBlocked(requestURL)) return } @@ -93,15 +90,21 @@ internal class WKRPageNavigation: NSObject, WKNavigationDelegate { if navigationAction.navigationType == .other { decisionHandler(.allow) } else { - pageStartedLoading() + pageUpdate(.startedLoading) - WKRPageFetcher.fetchSource(url: requestURL) { (source) in + WKRPageFetcher.fetchSource(url: requestURL, + useCache: true, + progressHandler: { progress in + self.pageUpdate(.loadingProgess(progress)) + }, completionHandler: { source, error in DispatchQueue.main.async { if let source = source { webView.loadHTMLString(source, baseURL: requestURL) + } else { + self.pageUpdate(.loadingError(error)) } } - } + }) decisionHandler(.cancel) } @@ -109,18 +112,16 @@ internal class WKRPageNavigation: NSObject, WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { if let url = webView.url { - pageLoaded(WKRPage(title: webView.title, url: url)) - } else { - fatalError("webView didFinish with no url") + pageUpdate(.loaded(WKRPage(title: webView.title, url: url))) } } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - pageLoadingError() + pageUpdate(.loadingError(error)) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - pageLoadingError() + pageUpdate(.loadingError(error)) } } diff --git a/WKRKit/WKRKitTests/WKRKitPageFetcherTests.swift b/WKRKit/WKRKitTests/WKRKitPageFetcherTests.swift index abffa2b..d196fe0 100644 --- a/WKRKit/WKRKitTests/WKRKitPageFetcherTests.swift +++ b/WKRKit/WKRKitTests/WKRKitPageFetcherTests.swift @@ -13,7 +13,7 @@ class WKRKitPageFetcherTests: WKRKitTestCase { func testConnectionTester() { self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { - let testExpectation = expectation(description: "testTester") + let testExpectation = expectation(description: "testConnectionTester") WKRConnectionTester.start { connected in XCTAssert(connected) testExpectation.fulfill() @@ -26,8 +26,8 @@ class WKRKitPageFetcherTests: WKRKitTestCase { func testError() { self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { - let testExpectation = expectation(description: "pageError") - WKRPageFetcher.fetch(path: "") { page in + let testExpectation = expectation(description: "testError") + WKRPageFetcher.fetch(path: "", useCache: false) { page, _ in XCTAssertNil(page) testExpectation.fulfill() } @@ -39,7 +39,7 @@ class WKRKitPageFetcherTests: WKRKitTestCase { func testRandom() { self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { - let testExpectation = expectation(description: "fetchRandom") + let testExpectation = expectation(description: "testRandom") WKRPageFetcher.fetchRandom { page in XCTAssertNotNil(page) guard let unwrappedPage = page else { @@ -64,8 +64,8 @@ class WKRKitPageFetcherTests: WKRKitTestCase { func testPage() { self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { - let testExpectation = expectation(description: "fetchRandom") - WKRPageFetcher.fetch(path: "/Apple_Inc.") { page in + let testExpectation = expectation(description: "testPage") + WKRPageFetcher.fetch(path: "/Apple_Inc.", useCache: false) { page, _ in XCTAssertNotNil(page) guard let unwrappedPage = page else { XCTFail("Page nil") @@ -88,8 +88,8 @@ class WKRKitPageFetcherTests: WKRKitTestCase { func testURL() { self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { - let testExpectation = expectation(description: "fetchRandom") - WKRPageFetcher.fetch(url: URL(string: "https://en.m.wikipedia.org/wiki/Apple_Inc.")!) { page in + let testExpectation = expectation(description: "testURL") + WKRPageFetcher.fetch(url: URL(string: "https://en.m.wikipedia.org/wiki/Apple_Inc.")!, useCache: false) { page, _ in XCTAssertNotNil(page) guard let unwrappedPage = page else { XCTFail("Page nil") @@ -112,8 +112,8 @@ class WKRKitPageFetcherTests: WKRKitTestCase { func testSource() { self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { - let testExpectation = expectation(description: "fetchSource") - WKRPageFetcher.fetchSource(url: URL(string: "https://en.m.wikipedia.org/wiki/Apple_Inc.")!) { source in + let testExpectation = expectation(description: "testSource") + WKRPageFetcher.fetchSource(url: URL(string: "https://en.m.wikipedia.org/wiki/Apple_Inc.")!, useCache: false, progressHandler: { _ in }) { source, _ in XCTAssertNotNil(source) testExpectation.fulfill() } @@ -131,7 +131,7 @@ class WKRKitPageFetcherTests: WKRKitTestCase { let testExpectation = expectation(description: "testLinkedPageFetcher") - DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { XCTAssertGreaterThan(fetcher.foundURLs.count, 200) XCTAssertLessThan(fetcher.foundURLs.count, 800) testExpectation.fulfill() @@ -142,4 +142,32 @@ class WKRKitPageFetcherTests: WKRKitTestCase { }) } } + + func testRedirect() { + self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { + let testExpectation = expectation(description: "testRedirect") + WKRPageFetcher.fetch(path: "/USA", useCache: false) { page, isRedirect in + XCTAssertNotNil(page) + XCTAssertTrue(isRedirect) + testExpectation.fulfill() + } + waitForExpectations(timeout: 10.0, handler: { _ in + self.stopMeasuring() + }) + } + } + + func testNotRedirect() { + self.measureMetrics([.wallClockTime], automaticallyStartMeasuring: true) { + let testExpectation = expectation(description: "testRedirect") + WKRPageFetcher.fetch(path: "/United_States", useCache: false) { page, isRedirect in + XCTAssertNotNil(page) + XCTAssertFalse(isRedirect) + testExpectation.fulfill() + } + waitForExpectations(timeout: 10.0, handler: { _ in + self.stopMeasuring() + }) + } + } } diff --git a/WKRKit/WKRKitTests/WKRKitRaceTests.swift b/WKRKit/WKRKitTests/WKRKitRaceTests.swift index f6bc05c..6237ae6 100644 --- a/WKRKit/WKRKitTests/WKRKitRaceTests.swift +++ b/WKRKit/WKRKitTests/WKRKitRaceTests.swift @@ -13,7 +13,7 @@ class WKRKitRaceTests: WKRKitTestCase { func testPreRaceFetch() { let testExpectation = expectation(description: "finalPage") - WKRPreRaceConfig.new { preRaceConfig in + WKRPreRaceConfig.new { preRaceConfig, _ in XCTAssertNotNil(preRaceConfig) XCTAssert(preRaceConfig?.voteInfo.pageCount == WKRKitConstants.current.votingArticlesCount) diff --git a/WKRKit/WKRKitTests/WKRKitTestCase.swift b/WKRKit/WKRKitTests/WKRKitTestCase.swift index 3c2790f..9891a8a 100644 --- a/WKRKit/WKRKitTests/WKRKitTestCase.swift +++ b/WKRKit/WKRKitTests/WKRKitTestCase.swift @@ -14,7 +14,10 @@ class WKRKitTestCase: XCTestCase { override func setUp() { super.setUp() WKRKitConstants.updateConstants() - XCTAssertEqual(WKRKitConstants.current.version, 11, "Installed WKRKitConstants not version 11") + let expectedVersion = 20 + XCTAssertEqual(WKRKitConstants.current.version, + expectedVersion, + "Installed WKRKitConstants not version \(expectedVersion)") } override func tearDown() { diff --git a/WKRKit/WKRKitTests/WKRKitTests.swift b/WKRKit/WKRKitTests/WKRKitTests.swift index 119144a..4d59937 100644 --- a/WKRKit/WKRKitTests/WKRKitTests.swift +++ b/WKRKit/WKRKitTests/WKRKitTests.swift @@ -68,14 +68,14 @@ class WKRKitTests: WKRKitTestCase { let playerOne = WKRPlayer.mock(named: "Andrew") playerOne.startedNewRace(on: starting) - playerOne.finishedViewingLastPage() + playerOne.finishedViewingLastPage(pixelsScrolled: 0) race.playerUpdated(playerOne) XCTAssertEqual(race.players.count, 1) let playerTwo = WKRPlayer.mock(named: "Midnight") playerTwo.startedNewRace(on: starting) sleep(1) - playerTwo.finishedViewingLastPage() + playerTwo.finishedViewingLastPage(pixelsScrolled: 0) race.playerUpdated(playerTwo) XCTAssertEqual(race.players.count, 2) @@ -101,16 +101,28 @@ class WKRKitTests: WKRKitTestCase { XCTAssertNil(points[playerThree.profile]) // same page - XCTAssertTrue(race.attributesFor(ending).foundPage) + XCTAssertTrue(race.attributes(for: ending).foundPage) // dif title, same url - XCTAssertTrue(race.attributesFor(WKRPage(title: "DifTitle", url: ending.url)).foundPage) + XCTAssertTrue(race.attributes(for: WKRPage(title: "DifTitle", url: ending.url)).foundPage) // same title, dif url - XCTAssertTrue(race.attributesFor(WKRPage(title: "Apple", url: starting.url)).foundPage) + XCTAssertTrue(race.attributes(for: WKRPage(title: "Apple", url: starting.url)).foundPage) + +// https://en.m.wikipedia.org/wiki/Forward_pass#American_and_Canadian_football + + if let url = URL(string: ending.url.absoluteString + "#section") { + // same title, url w/ section + XCTAssertTrue(race.attributes(for: WKRPage(title: "Apple", url: url)).foundPage) + + // dif title, url w/ section + XCTAssertTrue(race.attributes(for: WKRPage(title: "DifTitle", url: url)).foundPage) + } else { + XCTFail("url nil") + } // dif title, dif url - XCTAssertFalse(race.attributesFor(WKRPage(title: "Dif", url: URL(string: "http://a.com")!)).foundPage) + XCTAssertFalse(race.attributes(for: WKRPage(title: "Dif", url: URL(string: "http://a.com")!)).foundPage) } // MARK: - WKRInt @@ -191,7 +203,7 @@ class WKRKitTests: WKRKitTestCase { let page = WKRPage.mockApple() player.startedNewRace(on: page) - player.finishedViewingLastPage() + player.finishedViewingLastPage(pixelsScrolled: 0) do { let data = try JSONEncoder().encode(player) @@ -209,7 +221,7 @@ class WKRKitTests: WKRKitTestCase { WKRKitConstants.updateConstants() let version = WKRKitConstants.current.version - XCTAssertEqual(WKRKitConstants.current.version, 11) + XCTAssertEqual(WKRKitConstants.current.version, 20) WKRKitConstants.removeConstants() WKRKitConstants.updateConstants() @@ -243,10 +255,6 @@ class WKRKitTests: WKRKitTestCase { var page = WKRPage(title: title, url: url) XCTAssertEqual(page.title, title) - title = "phone" - page = WKRPage(title: title, url: url) - XCTAssertEqual(page.title, title.capitalized) - title = "iPhone - Wikipedia" page = WKRPage(title: title, url: url) XCTAssertEqual(page.title, "iPhone") @@ -257,13 +265,9 @@ class WKRKitTests: WKRKitTestCase { // Testing removing 10 characters WKRKitConstants.updateConstantsForTestingCharacterClipping() - title = "phone" - page = WKRPage(title: title, url: url) - XCTAssertEqual(page.title, title.capitalized) - title = "phone- Extra Characters" page = WKRPage(title: title, url: url) - XCTAssertEqual(page.title, "Phone- Extra ") + XCTAssertEqual(page.title, "phone- Extra ") title = "iPhone - Extra Characters" page = WKRPage(title: title, url: url) @@ -329,4 +333,112 @@ class WKRKitTests: WKRKitTestCase { testEncoding(for: votingObject) } + // MARK: - WKRReadyStates + + func testReadyStates() { + let playerA = WKRPlayer.mock(named: "A") + let playerB = WKRPlayer.mock(named: "B") + let playerC = WKRPlayer.mock(named: "C") + playerA.state = .readyForNextRound + playerB.state = .racing + playerC.state = .racing + + let players = [ + playerA, + playerB, + playerC + ] + let readyStates = WKRReadyStates(players: players) + + XCTAssert(readyStates.isPlayerReady(playerA)) + XCTAssertFalse(readyStates.isPlayerReady(playerB)) + XCTAssertFalse(readyStates.areAllRacePlayersReady(racePlayers: players)) + + playerA.state = .readyForNextRound + playerB.state = .quit + playerC.state = .readyForNextRound + + XCTAssert(readyStates.isPlayerReady(playerA)) + XCTAssertFalse(readyStates.isPlayerReady(playerB)) + XCTAssert(readyStates.areAllRacePlayersReady(racePlayers: players)) + + playerA.state = .readyForNextRound + playerB.state = .forcedEnd + playerC.state = .readyForNextRound + + XCTAssert(readyStates.isPlayerReady(playerA)) + XCTAssertFalse(readyStates.isPlayerReady(playerB)) + XCTAssertFalse(readyStates.areAllRacePlayersReady(racePlayers: players)) + + playerA.state = .readyForNextRound + playerB.state = .readyForNextRound + playerC.state = .readyForNextRound + + XCTAssert(readyStates.isPlayerReady(playerA)) + XCTAssert(readyStates.isPlayerReady(playerB)) + XCTAssert(readyStates.areAllRacePlayersReady(racePlayers: players)) + + playerA.state = .readyForNextRound + playerB.state = .readyForNextRound + playerC.state = .racing + + XCTAssert(readyStates.isPlayerReady(playerA)) + XCTAssertFalse(readyStates.isPlayerReady(playerC)) + XCTAssert(readyStates.areAllRacePlayersReady(racePlayers: [playerA, playerB])) + + let playerACopy = WKRPlayer(profile: playerA.profile, isHost: false) + let playerBCopy = WKRPlayer(profile: playerB.profile, isHost: false) + let playerCCopy = WKRPlayer(profile: playerC.profile, isHost: false) + + playerA.state = .racing + playerB.state = .racing + playerC.state = .racing + playerACopy.state = .readyForNextRound + playerBCopy.state = .readyForNextRound + playerCCopy.state = .readyForNextRound + + XCTAssertFalse(readyStates.isPlayerReady(playerCCopy)) + XCTAssertFalse(readyStates.areAllRacePlayersReady(racePlayers: [ + playerACopy, + playerBCopy, + playerCCopy])) + + playerC.state = .readyForNextRound + XCTAssert(readyStates.isPlayerReady(playerCCopy)) + XCTAssertFalse(readyStates.areAllRacePlayersReady(racePlayers: [ + playerACopy, + playerBCopy, + playerCCopy])) + + playerA.state = .quit + playerB.state = .readyForNextRound + XCTAssertFalse(readyStates.isPlayerReady(playerACopy)) + XCTAssert(readyStates.isPlayerReady(playerBCopy)) + XCTAssert(readyStates.areAllRacePlayersReady(racePlayers: [ + playerACopy, + playerBCopy, + playerCCopy])) + + } + + // MARK: - WKRPlayerRaceStats + + func testPlayerRaceStats() { + let starting = WKRPage.mockApple(withSuffix: "1") + let playerOne = WKRPlayer.mock(named: "Andrew") + playerOne.startedNewRace(on: starting) + + var stats = WKRPlayerRaceStats(player: playerOne) + XCTAssertEqual(stats.raw.count, 2) + XCTAssertEqual(stats.raw[0].value, "-") + XCTAssertEqual(stats.raw[1].value, "0 Pixels") + + sleep(1) + + playerOne.finishedViewingLastPage(pixelsScrolled: 5) + stats = WKRPlayerRaceStats(player: playerOne) + XCTAssertEqual(stats.raw[0].value, "1 S") + XCTAssertEqual(stats.raw[1].value, "5 Pixels") + } + } diff --git a/WKRPython/ArticleCompiler.py b/WKRPython/ArticleCompiler.py index 2cdb541..7b38c46 100644 --- a/WKRPython/ArticleCompiler.py +++ b/WKRPython/ArticleCompiler.py @@ -1,39 +1,87 @@ -from os import listdir -from time import sleep -from os.path import join -from selenium import webdriver -from BeautifulSoup import BeautifulSoup - import re import random -import urllib2 +import urllib +import urllib.request + import plistlib -import subprocess + +from os import listdir, makedirs +from os.path import join, exists + +from time import sleep +from selenium import webdriver +from bs4 import BeautifulSoup + +MAX_PAGE_TITLE_LENGTH = 25 +ILLEGAL_PAGE_TITLE_ITEMS = [ + ":", + "#", + ",_", + "List", + "disambiguation", + "(", + "Outline" +] + + +def create_folders(): + directory = "/Users/andrewfinke/Desktop/WKRPython/" + if not exists(directory): + makedirs(directory) + + directory = "/Users/andrewfinke/Desktop/WKRPython/PageLinksTests/" + if not exists(directory): + makedirs(directory) + + directory = "/Users/andrewfinke/Desktop/WKRPython/NetworkTests/" + if not exists(directory): + makedirs(directory) -def isValidLink(link): - if len(link) > 25: +def is_valid_article(article): + if len(article) > MAX_PAGE_TITLE_LENGTH: return False - bad = [":", "#", ",_", "List", "disambiguation", "(", "Outline"] - for badItem in bad: - if badItem in link: + for illegal_item in ILLEGAL_PAGE_TITLE_ITEMS: + if illegal_item in article: return False return True -def validateArticles(articles): - validArticles = [] - for article in set(articles): - if isValidLink("/wiki/" + article): - validArticles.append(article) - else: - print(article) - return validArticles +def articles_properties(article): + url = "https://en.m.wikipedia.org/wiki" + article + try: + html_page = urllib.request.urlopen(url) + soup = BeautifulSoup(html_page) + allrows = soup.findAll('th') + userrows = [t for t in allrows if t.findAll(text=re.compile('Born'))] + is_person_article = len(userrows) > 0 + + dis_text = " page lists articles associated with the title " + is_disambiguation_article = dis_text in str(soup) + except: + print("ERROR: " + article) + return False, False + return is_person_article, is_disambiguation_article + + +def remove_articles_with_year(articles): + years = [str(i) for i in range(1000, 2100)] + clean_articles = [] + for article in articles: + is_clean = True + for year in years: + if year in article: + is_clean = False + break + if is_clean: + clean_articles.append(article) + return clean_articles -def combineArticlesAtPath(path): + +def load_articles_in_directory(path): articles = [] files = [join(path, f) for f in listdir(path)] @@ -43,7 +91,7 @@ def combineArticlesAtPath(path): return set(articles) -def commonTitleWords(articles): +def word_count_in_strings(articles): words = {} for article in articles: for word in article[1:].replace("_", " ").split(): @@ -54,21 +102,16 @@ def commonTitleWords(articles): return words -def commonTitleWordsAsString(articles): - words = commonTitleWords(articles) +def formatted_word_count_in_strings(articles): + words = word_count_in_strings(articles) string = "" for k, v in sorted(words.items(), reverse=True, key=lambda x: x[1]): string += u'{0}: {1}'.format(k, v) + "\n" - return string - # path = "/Users/andrewfinke/Desktop/NewMaster2.txt" - # text_file = open(path, "w") - # text_file.write(string) - # text_file.close() -def fetchRedirect(driver, article): +def fetch_redirect(driver, article): link = "https://en.m.wikipedia.org/wiki" + article driver.get(link) @@ -76,254 +119,168 @@ def fetchRedirect(driver, article): sleep(0.4) split = driver.current_url.split("/") - newArticle = "/" + split[len(split) - 1] + new_article = "/" + split[len(split) - 1] - if newArticle != article: - print("REDIRECT: " + newArticle) + if new_article != article: + print("REDIRECT: " + new_article) else: print("NO REDIRECT") - return newArticle - - -def preciseContains(articleToCheck, articles): - for article in articles: - if article.lower() == articleToCheck.lower(): - return True - return False - - -def preciseRemoveAllDups(articles): - print("preciseRemoveDups") - newArticles = [] - for article in articles: - if preciseContains(article, newArticles) == False: - newArticles.append(article) - else: - print("DUP: " + article) - return newArticles + return new_article -def fullNetworkingTest(articles): +def run_networking_test(articles): + create_folders() driver = webdriver.Firefox() - validArticles = [] - redirectArticles = [] - errorArticles = [] + normal_articles = [] + redirecting_articles = [] + error_articles = [] - def saveArticles(): - validPath = "/Users/andrewfinke/Desktop/FullTest/CValid.plist" - plistlib.writePlist(sorted(validArticles), validPath) + def save_results(): + path = "/Users/andrewfinke/Desktop/WKRPython/NetworkTests/NormalArticles.plist" + plistlib.writePlist(sorted(normal_articles), path) - redirectPath = "/Users/andrewfinke/Desktop/FullTest/CRedirect.plist" - plistlib.writePlist(sorted(redirectArticles), redirectPath) + path = "/Users/andrewfinke/Desktop/WKRPython/NetworkTests/RedirectingArticles.plist" + plistlib.writePlist(sorted(redirecting_articles), path) - errorPath = "/Users/andrewfinke/Desktop/FullTest/CError.plist" - plistlib.writePlist(sorted(errorArticles), errorPath) + path = "/Users/andrewfinke/Desktop/WKRPython/NetworkTests/ErrorArticles.plist" + plistlib.writePlist(sorted(error_articles), path) for article in articles: print(article) try: - html_page = urllib2.urlopen( - "https://en.m.wikipedia.org/wiki" + article) + url = "https://en.m.wikipedia.org/wiki" + article + html_page = urllib.request.urlopen(url) if "Redirected from" in str(BeautifulSoup(html_page)): print("POSSIBLE REDIRECT") - redirectArticles.append(fetchRedirect(driver, article)) + possible_redirect = fetch_redirect(driver, article) + redirecting_articles.append(possible_redirect) else: print("VALID") - validArticles.append(article) + normal_articles.append(article) - except urllib2.HTTPError as err: - print("ERROR") - errorArticles.append(article) + except urllib.error.HTTPError as err: + print("ERROR: " + str(err)) + error_articles.append(article) - if len(validArticles) % 10 == 0: - saveArticles() - saveArticles() + if len(normal_articles) % 10 == 0: + save_results() + save_results() driver.quit() -def randomArticles(articles): - for x in range(0, 300): - randomArticles = [] - while len(randomArticles) != 8: - randomArticle = random.choice(articles) - if randomArticle not in randomArticles: - randomArticles.append(randomArticle) - print("\n=========\n") - for article in randomArticles: - print(article[1:].replace("_", " ")) - -# '/Apple_Inc.' - - -def fetchPageLinks(pageName): - html_page = urllib2.urlopen("https://en.m.wikipedia.org/wiki/" + pageName) +def fetch_links_on_article(article): + url = "https://en.m.wikipedia.org/wiki" + article + html_page = urllib.request.urlopen(url) soup = BeautifulSoup(html_page) - validLinks = [] + + page_links = [] for link in soup.findAll('a', attrs={'href': re.compile("/wiki/")}): href = link.get('href') - if isValidLink(href[5:]): - validLinks.append(href[5:]) + prefix_removed = href[5:] + if is_valid_article(prefix_removed): + page_links.append(prefix_removed) else: print("NOT VALID: " + href) - return sorted(list(set(validLinks)), key=lambda s: s.lower()) + return sorted(list(set(page_links)), key=lambda s: s.lower()) -def fetchPagesThatLinkTo(page): +def fetch_links_to_article(article): url = "https://en.m.wikipedia.org/w/index.php?title=Special:WhatLinksHere" + \ - page + "&namespace=0&limit=500&hidetrans=1" + article + "&namespace=0&limit=500&hidetrans=1&hideredirs=1" try: - html_page = urllib2.urlopen(url) + html_page = urllib.request.urlopen(url) soup = str(BeautifulSoup(html_page)) return soup.count('/wiki/') - except urllib2.HTTPError as err: + except urllib.error.HTTPError as err: return 0 -def isDisambiguationPage(page): - url = "https://en.m.wikipedia.org/wiki" + page - try: - html_page = urllib2.urlopen(url) - soup = str(BeautifulSoup(html_page)) - if " page lists articles associated with the title " in soup: - return True - except urllib2.HTTPError as err: - return False - return False - - -def fullDisambiguationTests(articles): - print("fullDisambiguationTests") - - articlesDis = [] - progress = 0 - for article in articles: - progress += 1 - print(str(progress) + " / " + str(len(articles))) - if isDisambiguationPage(article): - articlesDis.append(article) - - path = "/Users/andrewfinke/Desktop/FullTest/DisambiguationLinks.plist" - plistlib.writePlist(articlesDis, path) - - -def fullPageLinkTests(articles): - print("fullPageLinkTests") +def run_pages_that_link_to_articles_test(articles): + create_folders() - articles50 = {} - articles100 = {} - articles150 = {} - articles250 = {} - articles350 = {} - articles500 = {} - articlesAll = {} + articles_50 = {} + articles_100 = {} + articles_150 = {} + articles_250 = {} + articles_350 = {} + articles_500 = {} + articles_all = {} progress = 0 for article in articles: progress += 1 print(str(progress) + " / " + str(len(articles))) - count = fetchPagesThatLinkTo(article) - articlesAll[article] = count + count = fetch_links_to_article(article) + articles_all[article] = count if count < 50: - articles50[article] = count + articles_50[article] = count elif count < 100: - articles100[article] = count + articles_100[article] = count elif count < 150: - articles150[article] = count + articles_150[article] = count elif count < 250: - articles250[article] = count + articles_250[article] = count elif count < 350: - articles350[article] = count + articles_350[article] = count else: - articles500[article] = count + articles_500[article] = count - path = "/Users/andrewfinke/Desktop/FullTest/50Links.plist" - plistlib.writePlist(articles50, path) + def save(page_link_articles, name): + path = "/Users/andrewfinke/Desktop/WKRPython/PageLinksTests/" + name + plistlib.writePlist(page_link_articles, path) - path = "/Users/andrewfinke/Desktop/FullTest/100Links.plist" - plistlib.writePlist(articles100, path) + save(articles_50, "50.plist") + save(articles_100, "100.plist") + save(articles_150, "150.plist") + save(articles_250, "250.plist") + save(articles_350, "350.plist") + save(articles_500, "500.plist") + save(articles_all, "All.plist") - path = "/Users/andrewfinke/Desktop/FullTest/150Links.plist" - plistlib.writePlist(articles150, path) - path = "/Users/andrewfinke/Desktop/FullTest/250Links.plist" - plistlib.writePlist(articles250, path) +def print_random_articles(articles, count): + results = sorted(random.sample(articles, count)) + print("\n=========\n") + for article in results: + print(article[1:].replace("_", " ")) - path = "/Users/andrewfinke/Desktop/FullTest/350Links.plist" - plistlib.writePlist(articles350, path) - path = "/Users/andrewfinke/Desktop/FullTest/500Links.plist" - plistlib.writePlist(articles500, path) +def remove_articles_from_articles(articles_to_remove, articles): + cleaned = [] + for article in articles: + if article not in articles_to_remove: + cleaned.append(article) + return cleaned - path = "/Users/andrewfinke/Desktop/FullTest/AllLinks.plist" - plistlib.writePlist(articlesAll, path) +def save_string_to_path(string, path): + text_file = open(path, "w") + text_file.write(string) + text_file.close() -def removeArticles(articlesToRemove, articles): - for article, value in articlesToRemove.iteritems(): - if value < 100: - if article in articles: - articles.remove(article) - else: - print(article) - return articles +def load_articles_at_path(path): + return plistlib.readPlist(path) -def isPersonArticle(article): - html_page = urllib2.urlopen("https://en.m.wikipedia.org/wiki" + article) - soup = BeautifulSoup(html_page) - allrows = soup.findAll('th') - userrows = [t for t in allrows if t.findAll(text=re.compile('Born'))] - return len(userrows) > 0 + +def save_articles_to_path(articles, path): + valid = [] + for article in articles: + if is_valid_article(article): + valid.append(article) + plistlib.writePlist(sorted(list(set(valid)), key=lambda s: s.lower( + )), path) if __name__ == "__main__": - path = "/Users/andrewfinke/Desktop/NewMasterA.plist" - new_articles = plistlib.readPlist( - path) + plistlib.readPlist("/Users/andrewfinke/Desktop/WKRArticlesDataoo.plist") - # - # fullNetworkingTest(old_articles) - - # tpath = "/Users/andrewfinke/Desktop/NewMaster2.txt" - # text_file = open(tpath, "w") - # text_file.write(commonTitleWordsAsString(old_articles)) - # text_file.close() - - # paths = [ - # "/Users/andrewfinke/Desktop/FullTest/AValid.plist", - # "/Users/andrewfinke/Desktop/FullTest/ARedirect.plist", - # "/Users/andrewfinke/Desktop/FullTest/BValid.plist", - # "/Users/andrewfinke/Desktop/FullTest/BRedirect.plist", - # "/Users/andrewfinke/Desktop/FullTest/CValid.plist", - # "/Users/andrewfinke/Desktop/FullTest/CRedirect.plist", - # ] - # - # old_articles = [] - # for path in paths: - # old_articles += plistlib.readPlist(path) - # - # new_articles = [] - # for article in old_articles: - # if isValidLink("/wiki" + article): - # new_articles.append(article) - # - - new_articles = preciseRemoveAllDups(new_articles) - - # # fullPageLinkTests(old_articles) - # # new_articles = [] - # # - # # for article in old_articles: - # # if "_in_" not in article and "_of_" not in article: - # # new_articles.append(article) - # # # if isPersonArticle(article): - # # # print("Person: " + article) - # # # else: - # # # new_articles.append(article) - # # - # # # updated_articles = removeArticles(old_articles, new_articles) - # # - # # # fullPageLinkTests(newArticles[5000:15000]) - # # # # print(articles) - plistlib.writePlist(sorted(new_articles, key=lambda s: s.lower( - )), "/Users/andrewfinke/Desktop/WKRArticlesData.plist") + articles = load_articles_at_path( + "/Users/andrewfinke/Desktop/WKRArticlesData.plist") + for i in range(0, 10): + print_random_articles(articles, 8) + + for article in articles: + if not is_valid_link(article): + print("!!") + print(article) + # articles = fetch_links_on_article("/List_of_Disney_theme_park_attractions") diff --git a/WKRPython/requirements.txt b/WKRPython/requirements.txt new file mode 100644 index 0000000..ff160f4 --- /dev/null +++ b/WKRPython/requirements.txt @@ -0,0 +1,32 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: osx-64 +asn1crypto=0.24.0=py37_0 +beautifulsoup4=4.7.1=py37_1 +ca-certificates=2019.1.23=0 +certifi=2018.11.29=py37_0 +cffi=1.11.5=py37h6174b99_1 +cryptography=2.5=py37ha12b0ac_0 +idna=2.8=py37_0 +libcxx=4.0.1=hcfea43d_1 +libcxxabi=4.0.1=hcfea43d_1 +libedit=3.1.20181209=hb402a30_0 +libffi=3.2.1=h475c297_4 +ncurses=6.1=h0a44026_1 +openssl=1.1.1a=h1de35cc_0 +pip=19.0.1=py37_0 +pycparser=2.19=py37_0 +pyopenssl=19.0.0=py37_0 +pysocks=1.6.8=py37_0 +python=3.7.2=haf84260_0 +readline=7.0=h1de35cc_5 +selenium=3.141.0=py37h1de35cc_0 +setuptools=40.8.0=py37_0 +six=1.12.0=py37_0 +soupsieve=1.7.1=py37_0 +sqlite=3.26.0=ha441bb4_0 +tk=8.6.8=ha441bb4_0 +urllib3=1.24.1=py37_0 +wheel=0.32.3=py37_0 +xz=5.2.4=h1de35cc_4 +zlib=1.2.11=h1de35cc_3 diff --git a/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist b/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist index 5c8c547..a571ac2 100644 --- a/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist +++ b/WKRUIKit/WKRUIKit (UI Catalog)/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 257 + 339 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift b/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift index d4a7ed3..d3eb1d7 100644 --- a/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift +++ b/WKRUIKit/WKRUIKit (UI Catalog)/ViewController.swift @@ -19,7 +19,7 @@ class ViewController: UIViewController { super.viewDidLoad() webView = WKRUIWebView() - webView.load(URLRequest(url: URL(string: "https://en.m.wikipedia.org/wiki/apple")!)) + webView.load(URLRequest(url: URL(string: "https://en.m.wikipedia.org/wiki/apple_inc")!)) webView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(webView) @@ -33,6 +33,12 @@ class ViewController: UIViewController { NSLayoutConstraint.activate(constraints) navigationController?.navigationBar.barStyle = .wkrStyle + + Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { (_) in + DispatchQueue.main.async { + self.title = self.webView.pixelsScrolled.description + } + } } } diff --git a/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj b/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj index 19dacee..cce3986 100644 --- a/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj +++ b/WKRUIKit/WKRUIKit.xcodeproj/project.pbxproj @@ -268,16 +268,17 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1000; - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Andrew Finke"; TargetAttributes = { 149FF7F21F362B0D000A5D96 = { CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1000; + LastSwiftMigration = 1020; ProvisioningStyle = Manual; }; 14A3D49E1F3634110038388F = { CreatedOnToolsVersion = 9.0; + LastSwiftMigration = 1020; }; }; }; @@ -468,7 +469,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -572,7 +573,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -597,7 +598,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -613,7 +614,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WKRUIKit--UI-Catalog-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -629,7 +630,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WKRUIKit--UI-Catalog-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme b/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme index b43004b..4fde2ea 100644 --- a/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme +++ b/WKRUIKit/WKRUIKit.xcodeproj/xcshareddata/xcschemes/WKRUIKit (UI Catalog).xcscheme @@ -1,6 +1,6 @@ Version - 15 + 16 diff --git a/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift b/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift index 95bcb31..073da03 100644 --- a/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift +++ b/WKRUIKit/WKRUIKit/Constants/WKRUIKitConstants.swift @@ -14,15 +14,15 @@ public struct WKRUIKitConstants { // MARK: - Not Updated OTA static let webViewAnimateInDuration = 0.25 - static let webViewAnimateOutDuration = 0.25 + static let webViewAnimateOutDuration = 0.15 static let progessViewAnimateOutDelay = 0.85 static let progessViewAnimateOutDuration = 0.4 static let alertLabelHeight: CGFloat = 30.0 - static let alertAnimateInDuration = 0.25 - static let alertAnimateOutDuration = 0.25 - public static let alertDefaultDuration = 5.0 + static let alertAnimateInDuration = 0.2 + static let alertAnimateOutDuration = 0.15 + public static let alertDefaultDuration = 3.0 // MARK: - Updated OTA @@ -57,20 +57,20 @@ public struct WKRUIKitConstants { return } - guard let recordConstantsAsset = record["ConstantsFile"] as? CKAsset, - let recordStyleScriptAsset = record["StyleScriptFile"] as? CKAsset, - let recordStyleScriptDarkAsset = record["StyleScriptDarkFile"] as? CKAsset, - let recordCleanScriptAsset = record["CleanScriptFile"] as? CKAsset, - let recordContentBlockerAsset = record["ContentBlockerFile"] as? CKAsset else { + guard let recordConstantsAssetURL = (record["ConstantsFile"] as? CKAsset)?.fileURL, + let recordStyleScriptAssetURL = (record["StyleScriptFile"] as? CKAsset)?.fileURL, + let recordStyleScriptDarkAssetURL = (record["StyleScriptDarkFile"] as? CKAsset)?.fileURL, + let recordCleanScriptAssetURL = (record["CleanScriptFile"] as? CKAsset)?.fileURL, + let recordContentBlockerAssetURL = (record["ContentBlockerFile"] as? CKAsset)?.fileURL else { return } DispatchQueue.main.async { - copyIfNewer(newConstantsFileURL: recordConstantsAsset.fileURL, - newStyleScriptFileURL: recordStyleScriptAsset.fileURL, - newStyleScriptDarkFileURL: recordStyleScriptDarkAsset.fileURL, - newCleanScriptFileURL: recordCleanScriptAsset.fileURL, - newContentBlockerFileURL: recordContentBlockerAsset.fileURL) + copyIfNewer(newConstantsFileURL: recordConstantsAssetURL, + newStyleScriptFileURL: recordStyleScriptAssetURL, + newStyleScriptDarkFileURL: recordStyleScriptDarkAssetURL, + newCleanScriptFileURL: recordCleanScriptAssetURL, + newContentBlockerFileURL: recordContentBlockerAssetURL) } } } @@ -182,7 +182,7 @@ public struct WKRUIKitConstants { internal func contentBlocker() -> String { guard let source = try? String(contentsOf: WKRUIKitConstants.documentsPath(for: "WKRContentBlocker.json")) else { - fatalError("Failed to load style script") + fatalError("Failed to load blocker script") } return source } diff --git a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift index 02ce836..09e7991 100644 --- a/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift +++ b/WKRUIKit/WKRUIKit/Extensions/WKRUIKit+Colors.swift @@ -44,7 +44,7 @@ extension UIColor { } public static var wkrLightTextColor: UIColor { - return WKRUIStyle.isDark ? .white : #colorLiteral(red: 136.0/255.0, green: 136.0/255.0, blue: 136.0/255.0, alpha: 1.0) + return WKRUIStyle.isDark ? #colorLiteral(red: 210.0/255.0, green: 210.0/255.0, blue: 210.0/255.0, alpha: 1.0) : #colorLiteral(red: 136.0/255.0, green: 136.0/255.0, blue: 136.0/255.0, alpha: 1.0) } public static var wkrMenuTopViewColor: UIColor { diff --git a/WKRUIKit/WKRUIKit/Info.plist b/WKRUIKit/WKRUIKit/Info.plist index c29132d..9958f82 100644 --- a/WKRUIKit/WKRUIKit/Info.plist +++ b/WKRUIKit/WKRUIKit/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.6 + 3.6.4 CFBundleVersion - 5461 + 9202 NSPrincipalClass diff --git a/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift b/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift index 6b4de4d..84aded0 100644 --- a/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift +++ b/WKRUIKit/WKRUIKit/Views/WKRUIAlertView.swift @@ -15,6 +15,8 @@ public class WKRUIAlertView: WKRUIBottomOverlayView { private struct WKRAlertMessage: Equatable { let text: String let duration: Double + let isRaceSpecific: Bool + let playHaptic: Bool } // MARK: - Properties @@ -71,8 +73,15 @@ public class WKRUIAlertView: WKRUIBottomOverlayView { // MARK: - Enqueuing Messages - public func enqueue(text: String, duration: Double = WKRUIKitConstants.alertDefaultDuration) { - let message = WKRAlertMessage(text: text, duration: duration) + public func enqueue(text: String, + duration: Double = WKRUIKitConstants.alertDefaultDuration, + isRaceSpecific: Bool, + playHaptic: Bool) { + + let message = WKRAlertMessage(text: text, + duration: duration, + isRaceSpecific: isRaceSpecific, + playHaptic: playHaptic) // Make sure message doesn't equal most recent in queue. // If queue empty, make sure message isn't the same as the one being displayed. @@ -94,6 +103,10 @@ public class WKRUIAlertView: WKRUIBottomOverlayView { self.dismiss() } + public func clearRaceSpecificMessages() { + queue = queue.filter({ !$0.isRaceSpecific }) + } + // MARK: - State private func present() { @@ -109,7 +122,9 @@ public class WKRUIAlertView: WKRUIBottomOverlayView { alertWindow.setNeedsLayout() alertWindow.bringSubviewToFront(self) - UINotificationFeedbackGenerator().notificationOccurred(.warning) + if message.playHaptic { + UINotificationFeedbackGenerator().notificationOccurred(.warning) + } UIView.animate(withDuration: WKRUIKitConstants.alertAnimateInDuration) { self.alertWindow.layoutIfNeeded() diff --git a/WKRUIKit/WKRUIKit/Views/WKRUIThinLineView.swift b/WKRUIKit/WKRUIKit/Views/WKRUIThinLineView.swift index 4cbb36c..0f34ee3 100644 --- a/WKRUIKit/WKRUIKit/Views/WKRUIThinLineView.swift +++ b/WKRUIKit/WKRUIKit/Views/WKRUIThinLineView.swift @@ -17,6 +17,8 @@ public class WKRUIThinLineView: UIView { alpha = 0.25 backgroundColor = UIColor.wkrTextColor translatesAutoresizingMaskIntoConstraints = false + + layer.cornerRadius = 1 } required public init?(coder aDecoder: NSCoder) { diff --git a/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js b/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js index ce36d03..deb575d 100644 --- a/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js +++ b/WKRUIKit/WKRUIKit/Views/Web View/WKRCleanScript.js @@ -1,3 +1,7 @@ +window.onscroll = function () { + webkit.messageHandlers.scrollY.postMessage(window.scrollY); +}; + function cleanPage() { console.log("WKRUIKit: cleanPage"); diff --git a/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift b/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift index f4d154e..28d613e 100644 --- a/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift +++ b/WKRUIKit/WKRUIKit/Views/Web View/WKRUIWebView.swift @@ -8,26 +8,68 @@ import WebKit -public class WKRUIWebView: WKWebView { +public class WKRUIWebView: WKWebView, WKScriptMessageHandler { + + // MARK: - Type + + // WKScriptMessageHandler leaks due to a retain cycle + private class ScriptMessageDelegate: NSObject, WKScriptMessageHandler { + + // MARK: - Properties + + weak var delegate: WKScriptMessageHandler? + + // MARK: - Initalization + + init(delegate: WKScriptMessageHandler) { + self.delegate = delegate + super.init() + } + + // MARK: - WKScriptMessageHandler + + func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { + delegate?.userContentController(userContentController, didReceive: message) + } + } // MARK: - Properties public var text: String? { set { - timeLabel.text = newValue + linkCountLabel.text = newValue } get { - return timeLabel.text + return linkCountLabel.text } } - private let timeLabel = UILabel() + private let linkCountLabel = UILabel() + private let loadingView = UIView() + private let slowConnectionLabel = UILabel() + public var progressView: WKRUIProgressView? { didSet { progressView?.isHidden = true } } + public private(set) var pixelsScrolled = 0 + private var lastPixelOffset = 0 + + // network progress (fetch raw html) vs render progress (load html + images) + private static let networkProgressWeight: Float = 0.7 + private var progressObservation: NSKeyValueObservation? + public var networkProgress: Float = 0.0 { + didSet { + DispatchQueue.main.async { + self.progressView?.setProgress(self.networkProgress * WKRUIWebView.networkProgressWeight, + animated: true) + } + } + } + // MARK: - Initialization public init() { @@ -36,6 +78,9 @@ public class WKRUIWebView: WKWebView { } super.init(frame: .zero, configuration: config) + let messageDelegate = ScriptMessageDelegate(delegate: self) + config.userContentController.add(messageDelegate, name: "scrollY") + isOpaque = false backgroundColor = UIColor.wkrBackgroundColor translatesAutoresizingMaskIntoConstraints = false @@ -49,35 +94,66 @@ public class WKRUIWebView: WKWebView { .typeIdentifier: kMonospacedNumbersSelector ] ] - let fontDescriptor = UIFont.boldSystemFont(ofSize: 100.0).fontDescriptor.addingAttributes( + let fontDescriptor = UIFont.systemFont(ofSize: 100, weight: .semibold).fontDescriptor.addingAttributes( [UIFontDescriptor.AttributeName.featureSettings: features] ) - timeLabel.text = "0" - timeLabel.textColor = UIColor.white - timeLabel.textAlignment = .center + loadingView.alpha = 0.0 + loadingView.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.5) + loadingView.translatesAutoresizingMaskIntoConstraints = false + addSubview(loadingView) + + linkCountLabel.text = "0" + linkCountLabel.textColor = UIColor.white + linkCountLabel.textAlignment = .center + linkCountLabel.numberOfLines = 0 - timeLabel.alpha = 0.0 - timeLabel.numberOfLines = 0 + linkCountLabel.adjustsFontSizeToFitWidth = true + linkCountLabel.font = UIFont(descriptor: fontDescriptor, size: 100.0) + linkCountLabel.translatesAutoresizingMaskIntoConstraints = false + loadingView.addSubview(linkCountLabel) - timeLabel.adjustsFontSizeToFitWidth = true - timeLabel.translatesAutoresizingMaskIntoConstraints = false - timeLabel.font = UIFont(descriptor: fontDescriptor, size: 100.0) - timeLabel.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.2954569777) + slowConnectionLabel.text = "IF YOU SEE THIS FOR > 10 SECONDS, PLEASE LMK." + slowConnectionLabel.textColor = UIColor.white + slowConnectionLabel.textAlignment = .center + slowConnectionLabel.numberOfLines = 0 - addSubview(timeLabel) + slowConnectionLabel.adjustsFontSizeToFitWidth = true + slowConnectionLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold) + slowConnectionLabel.translatesAutoresizingMaskIntoConstraints = false + loadingView.addSubview(slowConnectionLabel) + + slowConnectionLabel.isHidden = true // only show during development scrollView.decelerationRate = UIScrollView.DecelerationRate.normal let constraints = [ - timeLabel.topAnchor.constraint(equalTo: topAnchor), - timeLabel.bottomAnchor.constraint(equalTo: bottomAnchor), - timeLabel.leftAnchor.constraint(equalTo: leftAnchor), - timeLabel.rightAnchor.constraint(equalTo: rightAnchor) + loadingView.topAnchor.constraint(equalTo: topAnchor), + loadingView.bottomAnchor.constraint(equalTo: bottomAnchor), + loadingView.leftAnchor.constraint(equalTo: leftAnchor), + loadingView.rightAnchor.constraint(equalTo: rightAnchor), + + linkCountLabel.topAnchor.constraint(equalTo: loadingView.topAnchor), + linkCountLabel.bottomAnchor.constraint(equalTo: loadingView.bottomAnchor), + linkCountLabel.leftAnchor.constraint(equalTo: loadingView.leftAnchor), + linkCountLabel.rightAnchor.constraint(equalTo: loadingView.rightAnchor), + + slowConnectionLabel.bottomAnchor.constraint(equalTo: loadingView.safeAreaLayoutGuide.bottomAnchor, + constant: -20), + slowConnectionLabel.leftAnchor.constraint(equalTo: loadingView.leftAnchor), + slowConnectionLabel.rightAnchor.constraint(equalTo: loadingView.rightAnchor) ] NSLayoutConstraint.activate(constraints) - addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) + progressObservation = observe(\.estimatedProgress) { [weak self] webView, _ in + DispatchQueue.main.async { + let weight = WKRUIWebView.networkProgressWeight + // network progress (would be weight * 1.0 since must be complete) + weighted webview progress + let progress = weight + Float(webView.estimatedProgress) * (1 - weight) + self?.progressView?.setProgress(progress, animated: true) + } + } + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, @@ -89,8 +165,9 @@ public class WKRUIWebView: WKWebView { } deinit { - removeObserver(self, forKeyPath: "estimatedProgress") + progressObservation = nil NotificationCenter.default.removeObserver(self) + configuration.userContentController.removeScriptMessageHandler(forName: "scrollY") } // MARK: - State Updates @@ -104,11 +181,12 @@ public class WKRUIWebView: WKWebView { public func startedPageLoad() { progressView?.show() + lastPixelOffset = 0 isUserInteractionEnabled = false let duration = WKRUIKitConstants.webViewAnimateOutDuration UIView.animate(withDuration: duration) { - self.timeLabel.alpha = 1.0 + self.loadingView.alpha = 1.0 } } @@ -119,7 +197,7 @@ public class WKRUIWebView: WKWebView { let duration = WKRUIKitConstants.webViewAnimateInDuration UIView.animate(withDuration: duration, delay: 0.0, options: .beginFromCurrentState, animations: { - self.timeLabel.alpha = 0.0 + self.loadingView.alpha = 0.0 }, completion: nil) } @@ -128,7 +206,7 @@ public class WKRUIWebView: WKWebView { private static func raceConfig() -> WKWebViewConfiguration? { let config = WKWebViewConfiguration() config.selectionGranularity = .character - config.suppressesIncrementalRendering = true + config.suppressesIncrementalRendering = false config.allowsAirPlayForMediaPlayback = false config.allowsPictureInPictureMediaPlayback = false config.dataDetectorTypes = [] @@ -171,19 +249,17 @@ public class WKRUIWebView: WKWebView { return config } - // MARK: - Progress View - - public override func observeValue(forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey: Any]?, - context: UnsafeMutableRawPointer?) { + // MARK: - WKScriptMessageHandler - guard keyPath == "estimatedProgress", let progress = change?[.newKey] as? Double else { - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) - return + public func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { + guard let messageBody = message.body as? Int else { return } + switch message.name { + case "scrollY": + pixelsScrolled += abs(messageBody - lastPixelOffset) + lastPixelOffset = messageBody + default: return } - - progressView?.setProgress(Float(progress), animated: true) } } diff --git a/WikiRaces.xcworkspace/contents.xcworkspacedata b/WikiRaces.xcworkspace/contents.xcworkspacedata index 3df2c47..60f8a07 100644 --- a/WikiRaces.xcworkspace/contents.xcworkspacedata +++ b/WikiRaces.xcworkspace/contents.xcworkspacedata @@ -13,4 +13,7 @@ + + diff --git a/WikiRaces/Fastfile b/WikiRaces/Fastfile deleted file mode 100644 index 6018ea0..0000000 --- a/WikiRaces/Fastfile +++ /dev/null @@ -1 +0,0 @@ -opt_out_crash_reporting \ No newline at end of file diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Crashlytics b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Crashlytics index 887ea76..e5a85e3 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Crashlytics and b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Crashlytics differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Info.plist b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Info.plist index 1d56896..ab886b6 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Info.plist and b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/Info.plist differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/submit b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/submit index 42e7231..3fda5cf 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/submit and b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/submit differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/uploadDSYM b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/uploadDSYM index e584ac2..6586420 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/uploadDSYM and b/WikiRaces/Shared/Frameworks/Analytics/Crashlytics.framework/uploadDSYM differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FIRAnalyticsConnector.framework/FIRAnalyticsConnector b/WikiRaces/Shared/Frameworks/Analytics/FIRAnalyticsConnector.framework/FIRAnalyticsConnector new file mode 100755 index 0000000..1898de6c Binary files /dev/null and b/WikiRaces/Shared/Frameworks/Analytics/FIRAnalyticsConnector.framework/FIRAnalyticsConnector differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FIRAnalyticsConnector.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FIRAnalyticsConnector.framework/Modules/module.modulemap new file mode 100755 index 0000000..270ad21 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FIRAnalyticsConnector.framework/Modules/module.modulemap @@ -0,0 +1,10 @@ +framework module FIRAnalyticsConnector { + export * + module * { export * } + link "sqlite3" + link "z" + link framework "Security" + link framework "StoreKit" + link framework "SystemConfiguration" + link framework "UIKit" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Fabric b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Fabric index 5ac7848..ffaceb6 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Fabric and b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Fabric differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Info.plist b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Info.plist index a00e88d..2b862ba 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Info.plist and b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/Info.plist differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/uploadDSYM b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/uploadDSYM index 1581be8..57114f5 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/uploadDSYM and b/WikiRaces/Shared/Frameworks/Analytics/Fabric.framework/uploadDSYM differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/Firebase.h b/WikiRaces/Shared/Frameworks/Analytics/Firebase.h index 6461547..7bd5a31 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Firebase.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Firebase.h @@ -58,6 +58,14 @@ Firebase services work as intended." #import #endif + #if __has_include() + #import + #endif + + #if __has_include() + #import + #endif + #if __has_include() #import #endif diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/FirebaseABTesting b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/FirebaseABTesting new file mode 100755 index 0000000..abb4d3f Binary files /dev/null and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/FirebaseABTesting differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FIRExperimentController.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FIRExperimentController.h new file mode 100755 index 0000000..abd2959 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FIRExperimentController.h @@ -0,0 +1,48 @@ +#import + +#import "developers/mobile/abt/proto/ExperimentPayload.pbobjc.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRLifecycleEvents; + +/// The default experiment overflow policy, that is to discard the experiment with the oldest start +/// time when users start the experiment on the web console. +extern const ABTExperimentPayload_ExperimentOverflowPolicy FIRDefaultExperimentOverflowPolicy; + +/// This class is for Firebase services to handle experiments updates to Firebase Analytics. +/// Experiments can be set, cleared and updated through this controller. +@interface FIRExperimentController : NSObject + +/// Returns the FIRExperimentController singleton. ++ (FIRExperimentController *)sharedInstance; + +/// Updates the list of experiments. Experiments already existing in payloads are not affected, +/// whose state and payload is preserved. This method compares whether the experiments have changed +/// or not by their variant ID. This runs in a background queue. +/// @param origin The originating service affected by the experiment, it is defined at +/// Firebase Analytics FIREventOrigins.h. +/// @param events A list of event names to be used for logging experiment lifecycle events, +/// if they are not defined in the payload. +/// @param policy The policy to handle new experiments when slots are full. +/// @param lastStartTime The last known experiment start timestamp for this affected service. +/// (Timestamps are specified by the number of seconds from 00:00:00 UTC on 1 +/// January 1970.). +/// @param payloads List of experiment metadata. +- (void)updateExperimentsWithServiceOrigin:(NSString *)origin + events:(FIRLifecycleEvents *)events + policy:(ABTExperimentPayload_ExperimentOverflowPolicy)policy + lastStartTime:(NSTimeInterval)lastStartTime + payloads:(NSArray *)payloads; + +/// Returns the latest experiment start timestamp given a current latest timestamp and a list of +/// experiment payloads. Timestamps are specified by the number of seconds from 00:00:00 UTC on 1 +/// January 1970. +/// @param timestamp Current latest experiment start timestamp. If not known, affected service +/// should specify -1; +/// @param payloads List of experiment metadata. +- (NSTimeInterval)latestExperimentStartTimestampBetweenTimestamp:(NSTimeInterval)timestamp + andPayloads:(NSArray *)payloads; +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FIRLifecycleEvents.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FIRLifecycleEvents.h new file mode 100755 index 0000000..a245a81 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FIRLifecycleEvents.h @@ -0,0 +1,46 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Default event name for when an experiment is set. +extern NSString *const FIRSetExperimentEventName; +/// Default event name for when an experiment is activated. +extern NSString *const FIRActivateExperimentEventName; +/// Default event name for when an experiment is cleared. +extern NSString *const FIRClearExperimentEventName; +/// Default event name for when an experiment times out for being activated. +extern NSString *const FIRTimeoutExperimentEventName; +/// Default event name for when an experiment is expired as it reaches the end of TTL. +extern NSString *const FIRExpireExperimentEventName; + +/// An Experiment Lifecycle Event Object that specifies the name of the experiment event to be +/// logged by Firebase Analytics. +@interface FIRLifecycleEvents : NSObject + +/// Event name for when an experiment is set. It is default to FIRSetExperimentEventName and can be +/// overriden. If experiment payload has a valid string of this field, always use experiment +/// payload. +@property(nonatomic, copy) NSString *setExperimentEventName; + +/// Event name for when an experiment is activated. It is default to FIRActivateExperimentEventName +/// and can be overriden. If experiment payload has a valid string of this field, always use +/// experiment payload. +@property(nonatomic, copy) NSString *activateExperimentEventName; + +/// Event name for when an experiment is clearred. It is default to FIRClearExperimentEventName and +/// can be overriden. If experiment payload has a valid string of this field, always use experiment +/// payload. +@property(nonatomic, copy) NSString *clearExperimentEventName; +/// Event name for when an experiment is timeout from being STANDBY. It is default to +/// FIRTimeoutExperimentEventName and can be overriden. If experiment payload has a valid string +/// of this field, always use experiment payload. +@property(nonatomic, copy) NSString *timeoutExperimentEventName; + +/// Event name when an experiment is expired when it reaches the end of its TTL. +/// It is default to FIRExpireExperimentEventName and can be overriden. If experiment payload has a +/// valid string of this field, always use experiment payload. +@property(nonatomic, copy) NSString *expireExperimentEventName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FirebaseABTesting.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FirebaseABTesting.h new file mode 100755 index 0000000..0aad493 --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Headers/FirebaseABTesting.h @@ -0,0 +1,2 @@ +#import "FIRExperimentController.h" +#import "FIRLifecycleEvents.h" diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Modules/module.modulemap new file mode 100755 index 0000000..603022f --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseABTesting.framework/Modules/module.modulemap @@ -0,0 +1,7 @@ +framework module FirebaseABTesting { + umbrella header "FirebaseABTesting.h" + export * + module * { export *} + link "z" + link framework "Security" + link framework "SystemConfiguration"} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/FirebaseAnalytics b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/FirebaseAnalytics index d3c9887..b5d8a4a 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/FirebaseAnalytics and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/FirebaseAnalytics differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRAnalytics.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRAnalytics.h index 39d23f1..afb9f82 100755 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRAnalytics.h +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRAnalytics.h @@ -107,6 +107,19 @@ NS_SWIFT_NAME(Analytics) + (void)setScreenName:(nullable NSString *)screenName screenClass:(nullable NSString *)screenClassOverride; +/// Sets whether analytics collection is enabled for this app on this device. This setting is +/// persisted across app sessions. By default it is enabled. +/// +/// @param analyticsCollectionEnabled A flag that enables or disables Analytics collection. ++ (void)setAnalyticsCollectionEnabled:(BOOL)analyticsCollectionEnabled; + +/// Sets the interval of inactivity in seconds that terminates the current session. The default +/// value is 1800 seconds (30 minutes). +/// +/// @param sessionTimeoutInterval The custom time of inactivity in seconds before the current +/// session terminates. ++ (void)setSessionTimeoutInterval:(NSTimeInterval)sessionTimeoutInterval; + /// The unique ID for this instance of the application. + (NSString *)appInstanceID; diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRParameterNames.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRParameterNames.h index 4e1366c..ad9fff7 100755 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRParameterNames.h +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Headers/FIRParameterNames.h @@ -392,9 +392,21 @@ static NSString *const kFIRParameterShipping NS_SWIFT_NAME(AnalyticsParameterShi /// // ... /// }; /// +/// +/// This constant has been deprecated. Use Method constant instead. static NSString *const kFIRParameterSignUpMethod NS_SWIFT_NAME(AnalyticsParameterSignUpMethod) = @"sign_up_method"; +/// A particular approach used in an operation; for example, "facebook" or "email" in the context +/// of a sign_up or login event. (NSString). +///
+///     NSDictionary *params = @{
+///       kFIRParameterMethod : @"google",
+///       // ...
+///     };
+/// 
+static NSString *const kFIRParameterMethod NS_SWIFT_NAME(AnalyticsParameterMethod) = @"method"; + /// The origin of your traffic, such as an Ad network (for example, google) or partner (urban /// airship). Identify the advertiser, site, publication, etc. that is sending traffic to your /// property. Highly recommended (NSString). @@ -505,3 +517,16 @@ static NSString *const kFIRParameterLevelName NS_SWIFT_NAME(AnalyticsParameterLe /// }; /// static NSString *const kFIRParameterSuccess NS_SWIFT_NAME(AnalyticsParameterSuccess) = @"success"; + +/// Indicates that the associated event should either extend the current session +/// or start a new session if no session was active when the event was logged. +/// Specify YES to extend the current session or to start a new session; any +/// other value will not extend or start a session. +///
+///     NSDictionary *params = @{
+///       kFIRParameterExtendSession : @YES,
+///       // ...
+///     };
+/// 
+static NSString *const kFIRParameterExtendSession NS_SWIFT_NAME(AnalyticsParameterExtendSession) = + @"extend_session"; diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Modules/module.modulemap index ef80595..6118e37 100755 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseAnalytics.framework/Modules/module.modulemap @@ -1,10 +1,11 @@ framework module FirebaseAnalytics { umbrella header "FirebaseAnalytics.h" export * - module * { export *} + module * { export * } link "sqlite3" link "z" link framework "Security" link framework "StoreKit" link framework "SystemConfiguration" - link framework "UIKit"} + link framework "UIKit" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/FirebaseCore b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/FirebaseCore index 72ab6af..259ee58 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/FirebaseCore and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/FirebaseCore differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRAnalyticsConfiguration.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRAnalyticsConfiguration.h index ca1d32c..add4a38 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRAnalyticsConfiguration.h +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRAnalyticsConfiguration.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN * This class provides configuration fields for Firebase Analytics. */ NS_SWIFT_NAME(AnalyticsConfiguration) +DEPRECATED_MSG_ATTRIBUTE("Use these methods directly on the `Analytics` class.") @interface FIRAnalyticsConfiguration : NSObject /** @@ -30,10 +31,13 @@ NS_SWIFT_NAME(AnalyticsConfiguration) + (FIRAnalyticsConfiguration *)sharedInstance NS_SWIFT_NAME(shared()); /** + * Deprecated. * Sets the minimum engagement time in seconds required to start a new session. The default value * is 10 seconds. */ -- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval; +- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval + DEPRECATED_MSG_ATTRIBUTE( + "Sessions are started immediately. More information at https://bit.ly/2FU46av"); /** * Sets the interval of inactivity in seconds that terminates the current session. The default diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRConfiguration.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRConfiguration.h index 95bba5e..b88fcaf 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRConfiguration.h +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Headers/FIRConfiguration.h @@ -32,7 +32,9 @@ NS_SWIFT_NAME(FirebaseConfiguration) @property(class, nonatomic, readonly) FIRConfiguration *sharedInstance NS_SWIFT_NAME(shared); /** The configuration class for Firebase Analytics. */ -@property(nonatomic, readwrite) FIRAnalyticsConfiguration *analyticsConfiguration; +@property(nonatomic, readwrite) + FIRAnalyticsConfiguration *analyticsConfiguration DEPRECATED_MSG_ATTRIBUTE( + "Use the methods available here directly on the `Analytics` class."); /** * Sets the logging level for internal Firebase logging. Firebase will only log messages diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Modules/module.modulemap index 62dba51..c33728f 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCore.framework/Modules/module.modulemap @@ -1,6 +1,6 @@ framework module FirebaseCore { - umbrella header "FirebaseCore.h" - export * - module * { export *} +umbrella header "FirebaseCore.h" +export * +module * { export * } link framework "Foundation" } diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/FirebaseCoreDiagnostics b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/FirebaseCoreDiagnostics index d3a6139..784272d 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/FirebaseCoreDiagnostics and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/FirebaseCoreDiagnostics differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/Modules/module.modulemap index bbcb94e..ce076e0 100755 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCoreDiagnostics.framework/Modules/module.modulemap @@ -1,6 +1,7 @@ framework module FirebaseCoreDiagnostics { export * - module * { export *} + module * { export * } link "z" link framework "Security" - link framework "SystemConfiguration"} + link framework "SystemConfiguration" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/FirebaseCrash b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/FirebaseCrash deleted file mode 100755 index 04fd2bf..0000000 Binary files a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/FirebaseCrash and /dev/null differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FIRCrash.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FIRCrash.h deleted file mode 100755 index 52dc005..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FIRCrash.h +++ /dev/null @@ -1,36 +0,0 @@ -#import "FIRCrashLog.h" - -/** - * This class allows you to configure the Firebase Crash SDK. - * - * This SDK uses a Firebase Instance ID token to identify the app instance and periodically sends - * data to the Firebase backend. (see `[FIRInstanceID getIDWithHandler:]`). - * To stop the periodic sync, call `[FIRInstanceID deleteIDWithHandler:]` and - * either disable this SDK or set FIRCrash.crashCollectionEnabled to NO. - */ -DEPRECATED_MSG_ATTRIBUTE - ("Use Crashlytics instead. https://firebase.google.com/docs/crashlytics/get-started") -NS_SWIFT_NAME(Crash) -@interface FIRCrash : NSObject - -/** - * FirebaseCrash instance using the default FIRApp. - */ -+ (FIRCrash *)sharedInstance NS_SWIFT_NAME(sharedInstance()); - -/** - * Is crash reporting enabled? If crash reporting was previously enabled, the exception handler will - * remain installed, but crashes will not be transmitted. - * - * This setting is persisted, and is applied on future invocations of your application. Once - * explicitly set, it overrides any settings in your Info.plist. - * - * By default, crash reporting is enabled. If you need to change the default (for example, because - * you want to prompt the user before collecting crashes) set firebase_crash_enabled to false in - * your application's Info.plist. - */ -@property(nonatomic, assign, getter=isCrashCollectionEnabled) BOOL crashCollectionEnabled - DEPRECATED_MSG_ATTRIBUTE - ("Use Crashlytics instead. https://firebase.google.com/docs/crashlytics/get-started"); - -@end diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FIRCrashLog.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FIRCrashLog.h deleted file mode 100755 index 88a6581..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FIRCrashLog.h +++ /dev/null @@ -1,176 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * @abstract Logs a message to the Firebase Crash Reporter system. - * - * @discussion This method adds a message to the crash reporter - * logging system. The recent logs will be sent with the crash - * report when the application exits abnormally. Note that the - * timestamp of this message and the timestamp of the console - * message may differ by a few milliseconds. - * - * Messages should be brief as the total size of the message payloads - * is limited by the uploader and may change between releases of the - * crash reporter. Excessively long messages will be truncated - * safely but that will introduce a delay in submitting the message. - * - * @warning Raises an NSInvalidArgumentException if @p format is nil. - * - * @param format A format string. - * - * @param ap A variable argument list. - */ -FOUNDATION_EXTERN NS_FORMAT_FUNCTION(1, 0) -NS_SWIFT_UNAVAILABLE("Use `FirebaseCrashMessage(_:)` instead.") -void FIRCrashLogv(NSString *format, va_list ap) - DEPRECATED_MSG_ATTRIBUTE - ("Use Crashlytics instead. https://firebase.google.com/docs/crashlytics/get-started"); - -/** - * @abstract Logs a message to the Firebase Crash Reporter system. - * - * @discussion This method adds a message to the crash reporter - * logging system. The recent logs will be sent with the crash - * report when the application exits abnormally. Note that the - * timestamp of this message and the timestamp of the console - * message may differ by a few milliseconds. - * - * Messages should be brief as the total size of the message payloads - * is limited by the uploader and may change between releases of the - * crash reporter. Excessively long messages will be truncated - * safely but that will introduce a delay in submitting the message. - * - * @warning Raises an NSInvalidArgumentException if @p format is nil. - * - * @param format A format string. - * - * @param ... A comma-separated list of arguments to substitute into - * format. - * - * @see FIRCrashLogv(format, ap) - */ -FOUNDATION_STATIC_INLINE NS_FORMAT_FUNCTION(1, 2) -DEPRECATED_MSG_ATTRIBUTE - ("Use Crashlytics instead. https://firebase.google.com/docs/crashlytics/get-started") -void FIRCrashLog(NSString *format, ...) { - va_list ap; - - va_start(ap, format); - FIRCrashLogv(format, ap); - va_end(ap); -} - -/** - * @abstract Logs a message to the Firebase Crash Reporter system as - * well as NSLog(). - * - * @discussion This method adds a message to the crash reporter - * logging system. The recent logs will be sent with the crash - * report when the application exits abnormally. Note that the - * timestamp of this message and the timestamp of the console - * message may differ by a few milliseconds. - * - * Messages should be brief as the total size of the message payloads - * is limited by the uploader and may change between releases of the - * crash reporter. Excessively long messages will be truncated - * safely but that will introduce a delay in submitting the message. - * - * @warning Raises an NSInvalidArgumentException if @p format is nil. - * - * @param format A format string. - * - * @param ap A variable argument list. - */ -FOUNDATION_STATIC_INLINE NS_FORMAT_FUNCTION(1, 0) -DEPRECATED_MSG_ATTRIBUTE - ("Use Crashlytics instead. https://firebase.google.com/docs/crashlytics/get-started") -NS_SWIFT_NAME(FirebaseCrashNSLogv(_:_:)) -void FIRCrashNSLogv(NSString *format, va_list ap) { - va_list ap2; - - va_copy(ap2, ap); - NSLogv(format, ap); - FIRCrashLogv(format, ap2); - va_end(ap2); -} - -/** - * @abstract Logs a message to the Firebase Crash Reporter system as - * well as NSLog(). - * - * @discussion This method adds a message to the crash reporter - * logging system. The recent logs will be sent with the crash - * report when the application exits abnormally. Note that the - * timestamp of this message and the timestamp of the console - * message may differ by a few milliseconds. - * - * Messages should be brief as the total size of the message payloads - * is limited by the uploader and may change between releases of the - * crash reporter. Excessively long messages will be truncated - * safely but that will introduce a delay in submitting the message. - * - * @warning Raises an NSInvalidArgumentException if @p format is nil. - * - * @param format A format string. - * - * @param ... A comma-separated list of arguments to substitute into - * format. - * - * @see FIRCrashLogv(format, ap) - */ -FOUNDATION_STATIC_INLINE NS_FORMAT_FUNCTION(1, 2) -DEPRECATED_MSG_ATTRIBUTE - ("Use Crashlytics instead. https://firebase.google.com/docs/crashlytics/get-started") -void FIRCrashNSLog(NSString *format, ...) { - va_list ap; - - va_start(ap, format); - FIRCrashNSLogv(format, ap); - va_end(ap); -} - -/** - * @abstract Logs a message to the Firebase Crash Reporter system in - * a way that is easily called from Swift code. - * - * @discussion This method adds a message to the crash reporter - * logging system. Similar to FIRCrashLog, but with a call signature - * that is more Swift friendly. Unlike FIRCrashLog, callers - * use string interpolation instead of formatting arguments. - * - * @code - * public func mySwiftFunction() { - * let unexpected_number = 10; - * FIRCrashMessage("This number doesn't seem right: \(unexpected_number)"); - * } - * @endcode - * - * Messages should be brief as the total size of the message payloads - * is limited by the uploader and may change between releases of the - * crash reporter. Excessively long messages will be truncated - * safely but that will introduce a delay in submitting the message. - * - * @param message A log message - * - * @see FIRCrashLog(format, ...) - */ -FOUNDATION_STATIC_INLINE NS_SWIFT_NAME(FirebaseCrashMessage(_:)) -DEPRECATED_MSG_ATTRIBUTE - ("Use Crashlytics instead. https://firebase.google.com/docs/crashlytics/get-started") -void FIRCrashMessage(NSString *message) { - FIRCrashLog(@"%@", message); -} - -NS_ASSUME_NONNULL_END - -#ifdef FIRCRASH_REPLACE_NSLOG -#if defined(DEBUG) || defined(FIRCRASH_LOG_TO_CONSOLE) -#define NSLog(...) FIRCrashNSLog(__VA_ARGS__) -#define NSLogv(...) FIRCrashNSLogv(__VA_ARGS__) -#else -#define NSLog(...) FIRCrashLog(__VA_ARGS__) -#define NSLogv(...) FIRCrashLogv(__VA_ARGS__) -#endif -#endif diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FirebaseCrash.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FirebaseCrash.h deleted file mode 100755 index efdecc0..0000000 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Headers/FirebaseCrash.h +++ /dev/null @@ -1,2 +0,0 @@ -#import "FIRCrash.h" -#import "FIRCrashLog.h" diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/FirebaseInstanceID b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/FirebaseInstanceID old mode 100755 new mode 100644 index 733eccb..b4eb9f5 Binary files a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/FirebaseInstanceID and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/FirebaseInstanceID differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FIRInstanceID.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FIRInstanceID.h old mode 100755 new mode 100644 index 97777e1..d95995a --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FIRInstanceID.h +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FIRInstanceID.h @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #import NS_ASSUME_NONNULL_BEGIN diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FirebaseInstanceID.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FirebaseInstanceID.h old mode 100755 new mode 100644 index 053ec2b..78c9ef1 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FirebaseInstanceID.h +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Headers/FirebaseInstanceID.h @@ -1 +1,17 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #import "FIRInstanceID.h" diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Modules/module.modulemap old mode 100755 new mode 100644 index 2058956..791fd46 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseInstanceID.framework/Modules/module.modulemap @@ -1,6 +1,6 @@ framework module FirebaseInstanceID { - umbrella header "FirebaseInstanceID.h" - export * - module * { export *} +umbrella header "FirebaseInstanceID.h" +export * +module * { export * } link framework "Security" - link framework "SystemConfiguration"} +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/FirebasePerformance b/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/FirebasePerformance index 6c37fbb..8924a3d 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/FirebasePerformance and b/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/FirebasePerformance differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/Modules/module.modulemap index 25f5c43..91041cd 100755 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebasePerformance.framework/Modules/module.modulemap @@ -1,7 +1,7 @@ framework module FirebasePerformance { umbrella header "FirebasePerformance.h" export * - module * { export *} + module * { export * } link "c++" link "sqlite3" link "z" @@ -10,4 +10,5 @@ framework module FirebasePerformance { link framework "Security" link framework "StoreKit" link framework "SystemConfiguration" - link framework "UIKit"} + link framework "UIKit" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/FirebaseRemoteConfig b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/FirebaseRemoteConfig new file mode 100755 index 0000000..d860dcd Binary files /dev/null and b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/FirebaseRemoteConfig differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Headers/FIRRemoteConfig.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Headers/FIRRemoteConfig.h new file mode 100755 index 0000000..098f3ce --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Headers/FIRRemoteConfig.h @@ -0,0 +1,246 @@ +// +// FIRRemoteConfig.h +// Firebase Remote Config service SDK +// Copyright 2016 Google Inc. All rights reserved. +// +#import + +/// The Firebase Remote Config service default namespace, to be used if the API method does not +/// specify a different namespace. Use the default namespace if configuring from the Google Firebase +/// service. +extern NSString *const __nonnull FIRNamespaceGoogleMobilePlatform + NS_SWIFT_NAME(NamespaceGoogleMobilePlatform); + +/// Key used to manage throttling in NSError user info when the refreshing of Remote Config +/// parameter values (data) is throttled. The value of this key is the elapsed time since 1970, +/// measured in seconds. +extern NSString *const __nonnull FIRRemoteConfigThrottledEndTimeInSecondsKey + NS_SWIFT_NAME(RemoteConfigThrottledEndTimeInSecondsKey); + +/// Indicates whether updated data was successfully fetched. +typedef NS_ENUM(NSInteger, FIRRemoteConfigFetchStatus) { + /// Config has never been fetched. + FIRRemoteConfigFetchStatusNoFetchYet, + /// Config fetch succeeded. + FIRRemoteConfigFetchStatusSuccess, + /// Config fetch failed. + FIRRemoteConfigFetchStatusFailure, + /// Config fetch was throttled. + FIRRemoteConfigFetchStatusThrottled, +} NS_SWIFT_NAME(RemoteConfigFetchStatus); + +/// Remote Config error domain that handles errors when fetching data from the service. +extern NSString *const __nonnull FIRRemoteConfigErrorDomain NS_SWIFT_NAME(RemoteConfigErrorDomain); +/// Firebase Remote Config service fetch error. +typedef NS_ENUM(NSInteger, FIRRemoteConfigError) { + /// Unknown or no error. + FIRRemoteConfigErrorUnknown = 8001, + /// Frequency of fetch requests exceeds throttled limit. + FIRRemoteConfigErrorThrottled = 8002, + /// Internal error that covers all internal HTTP errors. + FIRRemoteConfigErrorInternalError = 8003, +} NS_SWIFT_NAME(RemoteConfigError); + +/// Enumerated value that indicates the source of Remote Config data. Data can come from +/// the Remote Config service, the DefaultConfig that is available when the app is first installed, +/// or a static initialized value if data is not available from the service or DefaultConfig. +typedef NS_ENUM(NSInteger, FIRRemoteConfigSource) { + FIRRemoteConfigSourceRemote, ///< The data source is the Remote Config service. + FIRRemoteConfigSourceDefault, ///< The data source is the DefaultConfig defined for this app. + FIRRemoteConfigSourceStatic, ///< The data doesn't exist, return a static initialized value. +} NS_SWIFT_NAME(RemoteConfigSource); + +/// Completion handler invoked by fetch methods when they get a response from the server. +/// +/// @param status Config fetching status. +/// @param error Error message on failure. +typedef void (^FIRRemoteConfigFetchCompletion)(FIRRemoteConfigFetchStatus status, + NSError *__nullable error) + NS_SWIFT_NAME(RemoteConfigFetchCompletion); + +#pragma mark - FIRRemoteConfigValue +/// This class provides a wrapper for Remote Config parameter values, with methods to get parameter +/// values as different data types. +NS_SWIFT_NAME(RemoteConfigValue) +@interface FIRRemoteConfigValue : NSObject +/// Gets the value as a string. +@property(nonatomic, readonly, nullable) NSString *stringValue; +/// Gets the value as a number value. +@property(nonatomic, readonly, nullable) NSNumber *numberValue; +/// Gets the value as a NSData object. +@property(nonatomic, readonly, nonnull) NSData *dataValue; +/// Gets the value as a boolean. +@property(nonatomic, readonly) BOOL boolValue; +/// Identifies the source of the fetched value. +@property(nonatomic, readonly) FIRRemoteConfigSource source; +@end + +#pragma mark - FIRRemoteConfigSettings +/// Firebase Remote Config settings. +NS_SWIFT_NAME(RemoteConfigSettings) +@interface FIRRemoteConfigSettings : NSObject +/// Indicates whether Developer Mode is enabled. +@property(nonatomic, readonly) BOOL isDeveloperModeEnabled; +/// Initializes FIRRemoteConfigSettings, which is used to set properties for custom settings. To +/// make custom settings take effect, pass the FIRRemoteConfigSettings instance to the +/// configSettings property of FIRRemoteConfig. +- (nonnull FIRRemoteConfigSettings *)initWithDeveloperModeEnabled:(BOOL)developerModeEnabled + NS_DESIGNATED_INITIALIZER; +@end + +#pragma mark - FIRRemoteConfig +/// Firebase Remote Config class. The shared instance method +remoteConfig can be created and used +/// to fetch, activate and read config results and set default config results. +NS_SWIFT_NAME(RemoteConfig) +@interface FIRRemoteConfig : NSObject +/// Last successful fetch completion time. +@property(nonatomic, readonly, strong, nullable) NSDate *lastFetchTime; +/// Last fetch status. The status can be any enumerated value from FIRRemoteConfigFetchStatus. +@property(nonatomic, readonly, assign) FIRRemoteConfigFetchStatus lastFetchStatus; +/// Config settings are custom settings. +@property(nonatomic, readwrite, strong, nonnull) FIRRemoteConfigSettings *configSettings; + +/// Returns the FIRRemoteConfig instance shared throughout your app. This singleton object contains +/// the complete set of Remote Config parameter values available to the app, including the Active +/// Config and Default Config. This object also caches values fetched from the Remote Config Server +/// until they are copied to the Active Config by calling activateFetched. +/// When you fetch values from the Remote Config Server using the default Firebase namespace +/// service, you should use this class method to create a shared instance of the FIRRemoteConfig +/// object to ensure that your app will function properly with the Remote Config Server and the +/// Firebase service. ++ (nonnull FIRRemoteConfig *)remoteConfig NS_SWIFT_NAME(remoteConfig()); + +/// Unavailable. Use +remoteConfig instead. +- (nonnull instancetype)init __attribute__((unavailable("Use +remoteConfig instead."))); + +#pragma mark - Fetch +/// Fetches Remote Config data with a callback. Call activateFetched to make fetched data available +/// to your app. +/// +/// Note: This method uses a Firebase Instance ID token to identify the app instance, and once it's +/// called, it periodically sends data to the Firebase backend. (see +/// `[FIRInstanceID getIDWithHandler:]`). +/// To stop the periodic sync, developers need to call `[FIRInstanceID deleteIDWithHandler:]` and +/// avoid calling this method again. +/// +/// @param completionHandler Fetch operation callback. +- (void)fetchWithCompletionHandler:(nullable FIRRemoteConfigFetchCompletion)completionHandler; + +/// Fetches Remote Config data and sets a duration that specifies how long config data lasts. +/// Call activateFetched to make fetched data available to your app. +/// +/// Note: This method uses a Firebase Instance ID token to identify the app instance, and once it's +/// called, it periodically sends data to the Firebase backend. (see +/// `[FIRInstanceID getIDWithHandler:]`). +/// To stop the periodic sync, developers need to call `[FIRInstanceID deleteIDWithHandler:]` and +/// avoid calling this method again. +/// +/// @param expirationDuration Duration that defines how long fetched config data is available, in +/// seconds. When the config data expires, a new fetch is required. +/// @param completionHandler Fetch operation callback. +- (void)fetchWithExpirationDuration:(NSTimeInterval)expirationDuration + completionHandler:(nullable FIRRemoteConfigFetchCompletion)completionHandler; + +#pragma mark - Apply +/// Applies Fetched Config data to the Active Config, causing updates to the behavior and appearance +/// of the app to take effect (depending on how config data is used in the app). +/// Returns true if there was a Fetched Config, and it was activated. +/// Returns false if no Fetched Config was found, or the Fetched Config was already activated. +- (BOOL)activateFetched; + +#pragma mark - Get Config +/// Enables access to configuration values by using object subscripting syntax. +/// This is used to get the config value of the default namespace. +///
+/// // Example:
+/// FIRRemoteConfig *config = [FIRRemoteConfig remoteConfig];
+/// FIRRemoteConfigValue *value = config[@"yourKey"];
+/// BOOL b = value.boolValue;
+/// NSNumber *number = config[@"yourKey"].numberValue;
+/// 
+- (nonnull FIRRemoteConfigValue *)objectForKeyedSubscript:(nonnull NSString *)key; + +/// Gets the config value of the default namespace. +/// @param key Config key. +- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key; + +/// Gets the config value of a given namespace. +/// @param key Config key. +/// @param aNamespace Config results under a given namespace. +- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key + namespace:(nullable NSString *)aNamespace; + +/// Gets the config value of a given namespace and a given source. +/// @param key Config key. +/// @param aNamespace Config results under a given namespace. +/// @param source Config value source. +- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key + namespace:(nullable NSString *)aNamespace + source:(FIRRemoteConfigSource)source; + +/// Gets all the parameter keys from a given source and a given namespace. +/// +/// @param source The config data source. +/// @param aNamespace The config data namespace. +/// @return An array of keys under the given source and namespace. +- (nonnull NSArray *)allKeysFromSource:(FIRRemoteConfigSource)source + namespace:(nullable NSString *)aNamespace; + +/// Returns the set of parameter keys that start with the given prefix, from the default namespace +/// in the active config. +/// +/// @param prefix The key prefix to look for. If prefix is nil or empty, returns all the +/// keys. +/// @return The set of parameter keys that start with the specified prefix. +- (nonnull NSSet *)keysWithPrefix:(nullable NSString *)prefix; + +/// Returns the set of parameter keys that start with the given prefix, from the given namespace in +/// the active config. +/// +/// @param prefix The key prefix to look for. If prefix is nil or empty, returns all the +/// keys in the given namespace. +/// @param aNamespace The namespace in which to look up the keys. If the namespace is invalid, +/// returns an empty set. +/// @return The set of parameter keys that start with the specified prefix. +- (nonnull NSSet *)keysWithPrefix:(nullable NSString *)prefix + namespace:(nullable NSString *)aNamespace; + +#pragma mark - Defaults +/// Sets config defaults for parameter keys and values in the default namespace config. +/// +/// @param defaults A dictionary mapping a NSString * key to a NSObject * value. +- (void)setDefaults:(nullable NSDictionary *)defaults; + +/// Sets config defaults for parameter keys and values in the default namespace config. +/// +/// @param defaults A dictionary mapping a NSString * key to a NSObject * value. +/// @param aNamespace Config under a given namespace. +- (void)setDefaults:(nullable NSDictionary *)defaults + namespace:(nullable NSString *)aNamespace; + +/// Sets default configs from plist for default namespace; +/// @param fileName The plist file name, with no file name extension. For example, if the plist file +/// is defaultSamples.plist, call: +/// [[FIRRemoteConfig remoteConfig] setDefaultsFromPlistFileName:@"defaultSamples"]; +- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName + NS_SWIFT_NAME(setDefaults(fromPlist:)); + +/// Sets default configs from plist for a given namespace; +/// @param fileName The plist file name, with no file name extension. For example, if the plist file +/// is defaultSamples.plist, call: +/// [[FIRRemoteConfig remoteConfig] setDefaultsFromPlistFileName:@"defaultSamples"]; +/// @param aNamespace The namespace where the default config is set. +- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName + namespace:(nullable NSString *)aNamespace + NS_SWIFT_NAME(setDefaults(fromPlist:namespace:)); + +/// Returns the default value of a given key and a given namespace from the default config. +/// +/// @param key The parameter key of default config. +/// @param aNamespace The namespace of default config. +/// @return Returns the default value of the specified key and namespace. Returns +/// nil if the key or namespace doesn't exist in the default config. +- (nullable FIRRemoteConfigValue *)defaultValueForKey:(nullable NSString *)key + namespace:(nullable NSString *)aNamespace; + +@end diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Headers/FirebaseRemoteConfig.h b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Headers/FirebaseRemoteConfig.h new file mode 100755 index 0000000..eedc4fc --- /dev/null +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Headers/FirebaseRemoteConfig.h @@ -0,0 +1 @@ +#import "FIRRemoteConfig.h" diff --git a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Modules/module.modulemap similarity index 61% rename from WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Modules/module.modulemap rename to WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Modules/module.modulemap index b8cab15..bfabd85 100755 --- a/WikiRaces/Shared/Frameworks/Analytics/FirebaseCrash.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/FirebaseRemoteConfig.framework/Modules/module.modulemap @@ -1,11 +1,9 @@ -framework module FirebaseCrash { - umbrella header "FirebaseCrash.h" +framework module FirebaseRemoteConfig { + umbrella header "FirebaseRemoteConfig.h" export * module * { export *} - link "c++" link "sqlite3" link "z" - link framework "CoreTelephony" link framework "Security" link framework "StoreKit" link framework "SystemConfiguration" diff --git a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/GTMSessionFetcher b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/GTMSessionFetcher index 24d5910..5a3361f 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/GTMSessionFetcher and b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/GTMSessionFetcher differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcher.h b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcher.h index 56eb1ca..7f5fcda 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcher.h +++ b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcher.h @@ -734,14 +734,6 @@ NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NS // affect a fetch after the fetch has begun. - (void)setRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field; -// The fetcher's request (deprecated.) -// -// Exposing a mutable object in the interface was convenient but a bad design decision due -// to thread-safety requirements. Clients should use the request property and -// setRequestValue:forHTTPHeaderField: instead. -@property(atomic, readonly, GTM_NULLABLE) NSMutableURLRequest *mutableRequest - GTMSESSION_DEPRECATE_ON_2016_SDKS("use 'request' or '-setRequestValue:forHTTPHeaderField:'"); - // Data used for resuming a download task. @property(atomic, readonly, GTM_NULLABLE) NSData *downloadResumeData; @@ -1247,7 +1239,11 @@ NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NS // can catch those. #ifdef __OBJC__ -#if DEBUG +// If asserts are entirely no-ops, the synchronization monitor is just a bunch +// of counting code that doesn't report exceptional circumstances in any way. +// Only build the synchronization monitor code if NS_BLOCK_ASSERTIONS is not +// defined or asserts are being logged instead. +#if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG) #define __GTMSessionMonitorSynchronizedVariableInner(varname, counter) \ varname ## counter #define __GTMSessionMonitorSynchronizedVariable(varname, counter) \ diff --git a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcherService.h b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcherService.h index a696ac7..fb743ca 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcherService.h +++ b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Headers/GTMSessionFetcherService.h @@ -153,7 +153,7 @@ extern NSString *const kGTMSessionFetcherServiceSessionKey; @interface GTMSessionFetcherService (TestingSupport) -// Convenience method to create a fetcher service for testing. +// Convenience methods to create a fetcher service for testing. // // Fetchers generated by this mock fetcher service will not perform any // network operation, but will invoke callbacks and provide the supplied data @@ -165,6 +165,9 @@ extern NSString *const kGTMSessionFetcherServiceSessionKey; // See the description of the testBlock property below. + (instancetype)mockFetcherServiceWithFakedData:(GTM_NULLABLE NSData *)fakedDataOrNil fakedError:(GTM_NULLABLE NSError *)fakedErrorOrNil; ++ (instancetype)mockFetcherServiceWithFakedData:(GTM_NULLABLE NSData *)fakedDataOrNil + fakedResponse:(NSHTTPURLResponse *)fakedResponse + fakedError:(GTM_NULLABLE NSError *)fakedErrorOrNil; // Spin the run loop and discard events (or, if not on the main thread, just sleep the thread) // until all running and delayed fetchers have completed. diff --git a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Modules/module.modulemap index a0449c5..7256640 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/GTMSessionFetcher.framework/Modules/module.modulemap @@ -1,6 +1,6 @@ framework module GTMSessionFetcher { - umbrella header "GTMSessionFetcher.h" - export * - module * { export *} +umbrella header "GTMSessionFetcher.h" +export * +module * { export * } link framework "Security" } diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/GoogleAppMeasurement b/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/GoogleAppMeasurement index 5099d13..01ee574 100755 Binary files a/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/GoogleAppMeasurement and b/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/GoogleAppMeasurement differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/Modules/module.modulemap index ea1e687..b66fb64 100755 --- a/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/GoogleAppMeasurement.framework/Modules/module.modulemap @@ -1,9 +1,10 @@ framework module GoogleAppMeasurement { export * - module * { export *} + module * { export * } link "sqlite3" link "z" link framework "Security" link framework "StoreKit" link framework "SystemConfiguration" - link framework "UIKit"} + link framework "UIKit" +} diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/GoogleToolboxForMac b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/GoogleToolboxForMac index b206efb..c632e5c 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/GoogleToolboxForMac and b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/GoogleToolboxForMac differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMDefines.h b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMDefines.h index 7feb1cb..68ff8c0 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMDefines.h +++ b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMDefines.h @@ -336,29 +336,6 @@ #define GTM_NSSTRINGIFY(x) GTM_NSSTRINGIFY_INNER(x) #endif -// Macro to allow fast enumeration when building for 10.5 or later, and -// reliance on NSEnumerator for 10.4. Remember, NSDictionary w/ FastEnumeration -// does keys, so pick the right thing, nothing is done on the FastEnumeration -// side to be sure you're getting what you wanted. -#ifndef GTM_FOREACH_OBJECT - #if TARGET_OS_IPHONE || !(MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5) - #define GTM_FOREACH_ENUMEREE(element, enumeration) \ - for (element in enumeration) - #define GTM_FOREACH_OBJECT(element, collection) \ - for (element in collection) - #define GTM_FOREACH_KEY(element, collection) \ - for (element in collection) - #else - #define GTM_FOREACH_ENUMEREE(element, enumeration) \ - for (NSEnumerator *_ ## element ## _enum = enumeration; \ - (element = [_ ## element ## _enum nextObject]) != nil; ) - #define GTM_FOREACH_OBJECT(element, collection) \ - GTM_FOREACH_ENUMEREE(element, [collection objectEnumerator]) - #define GTM_FOREACH_KEY(element, collection) \ - GTM_FOREACH_ENUMEREE(element, [collection keyEnumerator]) - #endif -#endif - // ============================================================================ // GTM_SEL_STRING is for specifying selector (usually property) names to KVC diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSData+zlib.h b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSData+zlib.h index dceadc4..bb9e1b7 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSData+zlib.h +++ b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSData+zlib.h @@ -32,7 +32,7 @@ // // Uses the default compression level. + (NSData *)gtm_dataByGzippingBytes:(const void *)bytes - length:(NSUInteger)length; + length:(NSUInteger)length __attribute__((deprecated("Use error variant"))); + (NSData *)gtm_dataByGzippingBytes:(const void *)bytes length:(NSUInteger)length error:(NSError **)error; diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSDictionary+URLArguments.h b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSDictionary+URLArguments.h index 734edea..285a82c 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSDictionary+URLArguments.h +++ b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSDictionary+URLArguments.h @@ -29,12 +29,12 @@ /// NOTE: Apps targeting iOS 8 or OS X 10.10 and later should use /// NSURLComponents and NSURLQueryItem to create URLs with /// query arguments instead of using these category methods. -+ (NSDictionary *)gtm_dictionaryWithHttpArgumentsString:(NSString *)argString; ++ (NSDictionary *)gtm_dictionaryWithHttpArgumentsString:(NSString *)argString NS_DEPRECATED(10_0, 10_10, 2_0, 8_0, "Use NSURLComponents and NSURLQueryItem."); /// Gets a string representation of the dictionary in the form /// key1=value1&key2=value2&...&keyN=valueN, suitable for use as either /// URL arguments (after a '?') or POST body. Keys and values will be escaped /// automatically, so should be unescaped in the dictionary. -- (NSString *)gtm_httpArgumentsString; +- (NSString *)gtm_httpArgumentsString NS_DEPRECATED(10_0, 10_10, 2_0, 8_0, "Use NSURLComponents and NSURLQueryItem."); @end diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSString+URLArguments.h b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSString+URLArguments.h index 0cb7f30..08fe231 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSString+URLArguments.h +++ b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Headers/GTMNSString+URLArguments.h @@ -34,12 +34,12 @@ /// NOTE: Apps targeting iOS 8 or OS X 10.10 and later should use /// NSURLComponents and NSURLQueryItem to create properly-escaped /// URLs instead of using these category methods. -- (NSString*)gtm_stringByEscapingForURLArgument; +- (NSString*)gtm_stringByEscapingForURLArgument NS_DEPRECATED(10_0, 10_10, 2_0, 8_0, "Use NSURLComponents."); /// Returns the unescaped version of a URL argument /// /// This has the same behavior as stringByReplacingPercentEscapesUsingEncoding:, /// except that it will also convert '+' to space. -- (NSString*)gtm_stringByUnescapingFromURLArgument; +- (NSString*)gtm_stringByUnescapingFromURLArgument NS_DEPRECATED(10_0, 10_10, 2_0, 8_0, "Use NSURLComponents."); @end diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Modules/module.modulemap index e8ef72b..5334fe1 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/GoogleToolboxForMac.framework/Modules/module.modulemap @@ -1,6 +1,6 @@ framework module GoogleToolboxForMac { - umbrella header "GoogleToolboxForMac.h" - export * - module * { export *} +umbrella header "GoogleToolboxForMac.h" +export * +module * { export * } link "z" } diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/GoogleUtilities b/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/GoogleUtilities index c3d2f44..7941a7d 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/GoogleUtilities and b/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/GoogleUtilities differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/Modules/module.modulemap index c926765..ca7d94b 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/GoogleUtilities.framework/Modules/module.modulemap @@ -1,7 +1,7 @@ framework module GoogleUtilities { - umbrella header "GoogleUtilities.h" - export * - module * { export *} +umbrella header "GoogleUtilities.h" +export * +module * { export * } link framework "Security" link framework "SystemConfiguration" link "z" diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Any.pbobjc.h b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Any.pbobjc.h index ad26189..2091d72 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Any.pbobjc.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Any.pbobjc.h @@ -140,7 +140,8 @@ typedef GPB_ENUM(GPBAny_FieldNumber) { /** * A URL/resource name that uniquely identifies the type of the serialized - * protocol buffer message. The last segment of the URL's path must represent + * protocol buffer message. This string must contain at least + * one "/" character. The last segment of the URL's path must represent * the fully qualified name of the type (as in * `path/google.protobuf.Duration`). The name should be in a canonical form * (e.g., leading "." is not accepted). diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/FieldMask.pbobjc.h b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/FieldMask.pbobjc.h index 73296d5..72cac9a 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/FieldMask.pbobjc.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/FieldMask.pbobjc.h @@ -123,57 +123,49 @@ typedef GPB_ENUM(GPBFieldMask_FieldNumber) { * describe the updated values, the API ignores the values of all * fields not covered by the mask. * - * If a repeated field is specified for an update operation, the existing - * repeated values in the target resource will be overwritten by the new values. - * Note that a repeated field is only allowed in the last position of a `paths` - * string. + * If a repeated field is specified for an update operation, new values will + * be appended to the existing repeated field in the target resource. Note that + * a repeated field is only allowed in the last position of a `paths` string. * * If a sub-message is specified in the last position of the field mask for an - * update operation, then the existing sub-message in the target resource is - * overwritten. Given the target message: + * update operation, then new value will be merged into the existing sub-message + * in the target resource. + * + * For example, given the target message: * * f { * b { - * d : 1 - * x : 2 + * d: 1 + * x: 2 * } - * c : 1 + * c: [1] * } * * And an update message: * * f { * b { - * d : 10 + * d: 10 * } + * c: [2] * } * * then if the field mask is: * - * paths: "f.b" + * paths: ["f.b", "f.c"] * * then the result will be: * * f { * b { - * d : 10 + * d: 10 + * x: 2 * } - * c : 1 + * c: [1, 2] * } * - * However, if the update mask was: - * - * paths: "f.b.d" - * - * then the result would be: - * - * f { - * b { - * d : 10 - * x : 2 - * } - * c : 1 - * } + * An implementation may provide options to override this default behavior for + * repeated and message fields. * * In order to reset a field's value to the default, the field must * be in the mask and set to the default value in the provided resource. diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBArray.h b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBArray.h index 638b288..3d22cb8 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBArray.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBArray.h @@ -134,7 +134,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(int32_t value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(int32_t value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -146,7 +146,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(int32_t value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(int32_t value, NSUInteger idx, BOOL *stop))block; /** * Adds a value to this array. @@ -306,7 +306,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(uint32_t value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(uint32_t value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -318,7 +318,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(uint32_t value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(uint32_t value, NSUInteger idx, BOOL *stop))block; /** * Adds a value to this array. @@ -478,7 +478,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(int64_t value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(int64_t value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -490,7 +490,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(int64_t value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(int64_t value, NSUInteger idx, BOOL *stop))block; /** * Adds a value to this array. @@ -650,7 +650,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(uint64_t value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(uint64_t value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -662,7 +662,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(uint64_t value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(uint64_t value, NSUInteger idx, BOOL *stop))block; /** * Adds a value to this array. @@ -822,7 +822,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(float value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(float value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -834,7 +834,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(float value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(float value, NSUInteger idx, BOOL *stop))block; /** * Adds a value to this array. @@ -994,7 +994,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(double value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(double value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -1006,7 +1006,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(double value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(double value, NSUInteger idx, BOOL *stop))block; /** * Adds a value to this array. @@ -1166,7 +1166,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(BOOL value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(BOOL value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -1178,7 +1178,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(BOOL value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(BOOL value, NSUInteger idx, BOOL *stop))block; /** * Adds a value to this array. @@ -1369,7 +1369,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateValuesWithBlock:(void (^)(int32_t value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(int32_t value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -1381,7 +1381,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(int32_t value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(int32_t value, NSUInteger idx, BOOL *stop))block; // These methods bypass the validationFunc to provide access to values that were not // known at the time the binary was compiled. @@ -1403,7 +1403,7 @@ NS_ASSUME_NONNULL_BEGIN * **idx**: The index of the current value. * **stop**: A pointer to a boolean that when set stops the enumeration. **/ -- (void)enumerateRawValuesWithBlock:(void (^)(int32_t value, NSUInteger idx, BOOL *stop))block; +- (void)enumerateRawValuesWithBlock:(void (NS_NOESCAPE ^)(int32_t value, NSUInteger idx, BOOL *stop))block; /** * Enumerates the values on this array with the given block. @@ -1415,7 +1415,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateRawValuesWithOptions:(NSEnumerationOptions)opts - usingBlock:(void (^)(int32_t value, NSUInteger idx, BOOL *stop))block; + usingBlock:(void (NS_NOESCAPE ^)(int32_t value, NSUInteger idx, BOOL *stop))block; // If value is not a valid enumerator as defined by validationFunc, these // methods will assert in debug, and will log in release and assign the value @@ -1779,7 +1779,7 @@ NS_ASSUME_NONNULL_END //% * **idx**: The index of the current value. //% * **stop**: A pointer to a boolean that when set stops the enumeration. //% **/ -//%- (void)enumerateRawValuesWithBlock:(void (^)(TYPE value, NSUInteger idx, BOOL *stop))block; +//%- (void)enumerateRawValuesWithBlock:(void (NS_NOESCAPE ^)(TYPE value, NSUInteger idx, BOOL *stop))block; //% //%/** //% * Enumerates the values on this array with the given block. @@ -1791,7 +1791,7 @@ NS_ASSUME_NONNULL_END //% * **stop**: A pointer to a boolean that when set stops the enumeration. //% **/ //%- (void)enumerateRawValuesWithOptions:(NSEnumerationOptions)opts -//% usingBlock:(void (^)(TYPE value, NSUInteger idx, BOOL *stop))block; +//% usingBlock:(void (NS_NOESCAPE ^)(TYPE value, NSUInteger idx, BOOL *stop))block; //% //%// If value is not a valid enumerator as defined by validationFunc, these //%// methods will assert in debug, and will log in release and assign the value @@ -1821,7 +1821,7 @@ NS_ASSUME_NONNULL_END //% * **idx**: The index of the current value. //% * **stop**: A pointer to a boolean that when set stops the enumeration. //% **/ -//%- (void)enumerateValuesWithBlock:(void (^)(TYPE value, NSUInteger idx, BOOL *stop))block; +//%- (void)enumerateValuesWithBlock:(void (NS_NOESCAPE ^)(TYPE value, NSUInteger idx, BOOL *stop))block; //% //%/** //% * Enumerates the values on this array with the given block. @@ -1833,7 +1833,7 @@ NS_ASSUME_NONNULL_END //% * **stop**: A pointer to a boolean that when set stops the enumeration. //% **/ //%- (void)enumerateValuesWithOptions:(NSEnumerationOptions)opts -//% usingBlock:(void (^)(TYPE value, NSUInteger idx, BOOL *stop))block; +//% usingBlock:(void (NS_NOESCAPE ^)(TYPE value, NSUInteger idx, BOOL *stop))block; //%PDDM-DEFINE ARRAY_MUTABLE_INTERFACE(NAME, TYPE, HELPER_NAME) //%/** diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBBootstrap.h b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBBootstrap.h index ed53ae7..0ebca25 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBBootstrap.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBBootstrap.h @@ -91,6 +91,24 @@ #endif #endif +/** + * Attribute used for Objective-C proto interface deprecations without messages. + **/ +#ifndef GPB_DEPRECATED +#define GPB_DEPRECATED __attribute__((deprecated)) +#endif + +/** + * Attribute used for Objective-C proto interface deprecations with messages. + **/ +#ifndef GPB_DEPRECATED_MSG +#if __has_extension(attribute_deprecated_with_message) +#define GPB_DEPRECATED_MSG(msg) __attribute__((deprecated(msg))) +#else +#define GPB_DEPRECATED_MSG(msg) __attribute__((deprecated)) +#endif +#endif + // If property name starts with init we need to annotate it to get past ARC. // http://stackoverflow.com/questions/18723226/how-do-i-annotate-an-objective-c-property-with-an-objc-method-family/18723227#18723227 // diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDescriptor.h b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDescriptor.h index 651f4de..292bce1 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDescriptor.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDescriptor.h @@ -223,9 +223,12 @@ typedef NS_ENUM(uint8_t, GPBFieldType) { /** * Returns the enum value name for the given raw enum. * + * Note that there can be more than one name corresponding to a given value + * if the allow_alias option is used. + * * @param number The raw enum value. * - * @return The name of the enum value passed, or nil if not valid. + * @return The first name that matches the enum value passed, or nil if not valid. **/ - (nullable NSString *)enumNameForValue:(int32_t)number; @@ -244,7 +247,7 @@ typedef NS_ENUM(uint8_t, GPBFieldType) { * * @param number The raw enum value. * - * @return The text format name for the raw enum value, or nil if not valid. + * @return The first text format name which matches the enum value, or nil if not valid. **/ - (nullable NSString *)textFormatNameForValue:(int32_t)number; @@ -258,6 +261,33 @@ typedef NS_ENUM(uint8_t, GPBFieldType) { **/ - (BOOL)getValue:(nullable int32_t *)outValue forEnumTextFormatName:(NSString *)textFormatName; +/** + * Gets the number of defined enum names. + * + * @return Count of the number of enum names, including any aliases. + */ +@property(nonatomic, readonly) uint32_t enumNameCount; + +/** + * Gets the enum name corresponding to the given index. + * + * @param index Index into the available names. The defined range is from 0 + * to self.enumNameCount - 1. + * + * @returns The enum name at the given index, or nil if the index is out of range. + */ +- (nullable NSString *)getEnumNameForIndex:(uint32_t)index; + +/** + * Gets the enum text format name corresponding to the given index. + * + * @param index Index into the available names. The defined range is from 0 + * to self.enumNameCount - 1. + * + * @returns The text format name at the given index, or nil if the index is out of range. + */ +- (nullable NSString *)getEnumTextFormatNameForIndex:(uint32_t)index; + @end /** diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDictionary.h b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDictionary.h index a81165e..d00b5b3 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDictionary.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/GPBDictionary.h @@ -109,7 +109,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt32sUsingBlock: - (void (^)(uint32_t key, uint32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, uint32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -204,7 +204,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt32sUsingBlock: - (void (^)(uint32_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, int32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -299,7 +299,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt64sUsingBlock: - (void (^)(uint32_t key, uint64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, uint64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -394,7 +394,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt64sUsingBlock: - (void (^)(uint32_t key, int64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, int64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -489,7 +489,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndBoolsUsingBlock: - (void (^)(uint32_t key, BOOL value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, BOOL value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -584,7 +584,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndFloatsUsingBlock: - (void (^)(uint32_t key, float value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, float value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -679,7 +679,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndDoublesUsingBlock: - (void (^)(uint32_t key, double value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, double value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -795,7 +795,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndEnumsUsingBlock: - (void (^)(uint32_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, int32_t value, BOOL *stop))block; /** * Gets the raw enum value for the given key. @@ -822,7 +822,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndRawValuesUsingBlock: - (void (^)(uint32_t key, int32_t rawValue, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, int32_t rawValue, BOOL *stop))block; /** * Adds the keys and raw enum values from another dictionary. @@ -935,7 +935,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndObjectsUsingBlock: - (void (^)(uint32_t key, ObjectType object, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint32_t key, ObjectType object, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1030,7 +1030,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt32sUsingBlock: - (void (^)(int32_t key, uint32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, uint32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1125,7 +1125,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt32sUsingBlock: - (void (^)(int32_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, int32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1220,7 +1220,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt64sUsingBlock: - (void (^)(int32_t key, uint64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, uint64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1315,7 +1315,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt64sUsingBlock: - (void (^)(int32_t key, int64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, int64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1410,7 +1410,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndBoolsUsingBlock: - (void (^)(int32_t key, BOOL value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, BOOL value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1505,7 +1505,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndFloatsUsingBlock: - (void (^)(int32_t key, float value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, float value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1600,7 +1600,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndDoublesUsingBlock: - (void (^)(int32_t key, double value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, double value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1716,7 +1716,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndEnumsUsingBlock: - (void (^)(int32_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, int32_t value, BOOL *stop))block; /** * Gets the raw enum value for the given key. @@ -1743,7 +1743,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndRawValuesUsingBlock: - (void (^)(int32_t key, int32_t rawValue, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, int32_t rawValue, BOOL *stop))block; /** * Adds the keys and raw enum values from another dictionary. @@ -1856,7 +1856,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndObjectsUsingBlock: - (void (^)(int32_t key, ObjectType object, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int32_t key, ObjectType object, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -1951,7 +1951,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt32sUsingBlock: - (void (^)(uint64_t key, uint32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, uint32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2046,7 +2046,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt32sUsingBlock: - (void (^)(uint64_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, int32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2141,7 +2141,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt64sUsingBlock: - (void (^)(uint64_t key, uint64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, uint64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2236,7 +2236,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt64sUsingBlock: - (void (^)(uint64_t key, int64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, int64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2331,7 +2331,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndBoolsUsingBlock: - (void (^)(uint64_t key, BOOL value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, BOOL value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2426,7 +2426,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndFloatsUsingBlock: - (void (^)(uint64_t key, float value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, float value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2521,7 +2521,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndDoublesUsingBlock: - (void (^)(uint64_t key, double value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, double value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2637,7 +2637,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndEnumsUsingBlock: - (void (^)(uint64_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, int32_t value, BOOL *stop))block; /** * Gets the raw enum value for the given key. @@ -2664,7 +2664,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndRawValuesUsingBlock: - (void (^)(uint64_t key, int32_t rawValue, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, int32_t rawValue, BOOL *stop))block; /** * Adds the keys and raw enum values from another dictionary. @@ -2777,7 +2777,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndObjectsUsingBlock: - (void (^)(uint64_t key, ObjectType object, BOOL *stop))block; + (void (NS_NOESCAPE ^)(uint64_t key, ObjectType object, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2872,7 +2872,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt32sUsingBlock: - (void (^)(int64_t key, uint32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, uint32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -2967,7 +2967,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt32sUsingBlock: - (void (^)(int64_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, int32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3062,7 +3062,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt64sUsingBlock: - (void (^)(int64_t key, uint64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, uint64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3157,7 +3157,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt64sUsingBlock: - (void (^)(int64_t key, int64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, int64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3252,7 +3252,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndBoolsUsingBlock: - (void (^)(int64_t key, BOOL value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, BOOL value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3347,7 +3347,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndFloatsUsingBlock: - (void (^)(int64_t key, float value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, float value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3442,7 +3442,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndDoublesUsingBlock: - (void (^)(int64_t key, double value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, double value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3558,7 +3558,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndEnumsUsingBlock: - (void (^)(int64_t key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, int32_t value, BOOL *stop))block; /** * Gets the raw enum value for the given key. @@ -3585,7 +3585,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndRawValuesUsingBlock: - (void (^)(int64_t key, int32_t rawValue, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, int32_t rawValue, BOOL *stop))block; /** * Adds the keys and raw enum values from another dictionary. @@ -3698,7 +3698,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndObjectsUsingBlock: - (void (^)(int64_t key, ObjectType object, BOOL *stop))block; + (void (NS_NOESCAPE ^)(int64_t key, ObjectType object, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3793,7 +3793,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt32sUsingBlock: - (void (^)(BOOL key, uint32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, uint32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3888,7 +3888,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt32sUsingBlock: - (void (^)(BOOL key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, int32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -3983,7 +3983,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt64sUsingBlock: - (void (^)(BOOL key, uint64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, uint64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4078,7 +4078,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt64sUsingBlock: - (void (^)(BOOL key, int64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, int64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4173,7 +4173,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndBoolsUsingBlock: - (void (^)(BOOL key, BOOL value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, BOOL value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4268,7 +4268,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndFloatsUsingBlock: - (void (^)(BOOL key, float value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, float value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4363,7 +4363,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndDoublesUsingBlock: - (void (^)(BOOL key, double value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, double value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4479,7 +4479,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndEnumsUsingBlock: - (void (^)(BOOL key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, int32_t value, BOOL *stop))block; /** * Gets the raw enum value for the given key. @@ -4506,7 +4506,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndRawValuesUsingBlock: - (void (^)(BOOL key, int32_t rawValue, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, int32_t rawValue, BOOL *stop))block; /** * Adds the keys and raw enum values from another dictionary. @@ -4619,7 +4619,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndObjectsUsingBlock: - (void (^)(BOOL key, ObjectType object, BOOL *stop))block; + (void (NS_NOESCAPE ^)(BOOL key, ObjectType object, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4714,7 +4714,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt32sUsingBlock: - (void (^)(NSString *key, uint32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, uint32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4809,7 +4809,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt32sUsingBlock: - (void (^)(NSString *key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, int32_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4904,7 +4904,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndUInt64sUsingBlock: - (void (^)(NSString *key, uint64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, uint64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -4999,7 +4999,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndInt64sUsingBlock: - (void (^)(NSString *key, int64_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, int64_t value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -5094,7 +5094,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndBoolsUsingBlock: - (void (^)(NSString *key, BOOL value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, BOOL value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -5189,7 +5189,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndFloatsUsingBlock: - (void (^)(NSString *key, float value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, float value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -5284,7 +5284,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndDoublesUsingBlock: - (void (^)(NSString *key, double value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, double value, BOOL *stop))block; /** * Adds the keys and values from another dictionary. @@ -5400,7 +5400,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndEnumsUsingBlock: - (void (^)(NSString *key, int32_t value, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, int32_t value, BOOL *stop))block; /** * Gets the raw enum value for the given key. @@ -5427,7 +5427,7 @@ NS_ASSUME_NONNULL_BEGIN * **stop**: A pointer to a boolean that when set stops the enumeration. **/ - (void)enumerateKeysAndRawValuesUsingBlock: - (void (^)(NSString *key, int32_t rawValue, BOOL *stop))block; + (void (NS_NOESCAPE ^)(NSString *key, int32_t rawValue, BOOL *stop))block; /** * Adds the keys and raw enum values from another dictionary. @@ -5693,7 +5693,7 @@ NS_ASSUME_NONNULL_END //% * **stop**: A pointer to a boolean that when set stops the enumeration. //% **/ //%- (void)enumerateKeysAndRawValuesUsingBlock: -//% (void (^)(KEY_TYPE KisP##key, VALUE_TYPE rawValue, BOOL *stop))block; +//% (void (NS_NOESCAPE ^)(KEY_TYPE KisP##key, VALUE_TYPE rawValue, BOOL *stop))block; //% //%/** //% * Adds the keys and raw enum values from another dictionary. @@ -5728,7 +5728,7 @@ NS_ASSUME_NONNULL_END //% * **stop**: ##VNAME_VAR$S## A pointer to a boolean that when set stops the enumeration. //% **/ //%- (void)enumerateKeysAnd##VNAME##sUsingBlock: -//% (void (^)(KEY_TYPE KisP##key, VALUE_TYPE VNAME_VAR, BOOL *stop))block; +//% (void (NS_NOESCAPE ^)(KEY_TYPE KisP##key, VALUE_TYPE VNAME_VAR, BOOL *stop))block; //%PDDM-DEFINE DICTIONARY_MUTABLE_INTERFACE(KEY_NAME, KEY_TYPE, KisP, VALUE_NAME, VALUE_TYPE, VHELPER, VNAME, VNAME_VAR) //%/** diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Timestamp.pbobjc.h b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Timestamp.pbobjc.h index 2c4b8b2..f6ea25c 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Timestamp.pbobjc.h +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Headers/Timestamp.pbobjc.h @@ -56,17 +56,19 @@ typedef GPB_ENUM(GPBTimestamp_FieldNumber) { }; /** - * A Timestamp represents a point in time independent of any time zone - * or calendar, represented as seconds and fractions of seconds at - * nanosecond resolution in UTC Epoch time. It is encoded using the - * Proleptic Gregorian Calendar which extends the Gregorian calendar - * backwards to year one. It is encoded assuming all minutes are 60 - * seconds long, i.e. leap seconds are "smeared" so that no leap second - * table is needed for interpretation. Range is from - * 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. - * By restricting to that range, we ensure that we can convert to - * and from RFC 3339 date strings. - * See [https://www.ietf.org/rfc/rfc3339.txt](https://www.ietf.org/rfc/rfc3339.txt). + * A Timestamp represents a point in time independent of any time zone or local + * calendar, encoded as a count of seconds and fractions of seconds at + * nanosecond resolution. The count is relative to an epoch at UTC midnight on + * January 1, 1970, in the proleptic Gregorian calendar which extends the + * Gregorian calendar backwards to year one. + * + * All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + * second table is needed for interpretation, using a [24-hour linear + * smear](https://developers.google.com/time/smear). + * + * The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + * restricting to that range, we ensure that we can convert to and from [RFC + * 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. * * # Examples * @@ -127,12 +129,12 @@ typedef GPB_ENUM(GPBTimestamp_FieldNumber) { * 01:30 UTC on January 15, 2017. * * In JavaScript, one can convert a Date object to this format using the - * standard [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString] + * standard [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) * method. In Python, a standard `datetime.datetime` object can be converted * to this format using [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) * with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one * can use the Joda Time's [`ISODateTimeFormat.dateTime()`]( - * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime-- + * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D * ) to obtain a formatter capable of generating timestamps in this format. **/ @interface GPBTimestamp : GPBMessage diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Modules/module.modulemap index 8b1cb3f..880e9c4 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Modules/module.modulemap @@ -1,5 +1,5 @@ framework module Protobuf { - umbrella header "Protobuf.h" - export * - module * { export *} +umbrella header "Protobuf.h" +export * +module * { export * } } diff --git a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Protobuf b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Protobuf index 2ac06a4..a64e607 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Protobuf and b/WikiRaces/Shared/Frameworks/Analytics/Protobuf.framework/Protobuf differ diff --git a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb.h b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb.h index bf05a63..174a84b 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb.h +++ b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb.h @@ -46,7 +46,7 @@ /* Version of the nanopb library. Just in case you want to check it in * your own program. */ -#define NANOPB_VERSION nanopb-0.3.8 +#define NANOPB_VERSION nanopb-0.3.9.1 /* Include all the system headers needed by nanopb. You will need the * definitions of the following: @@ -251,8 +251,10 @@ PB_PACKED_STRUCT_END * If you get errors here, it probably means that your stdint.h is not * correct for your platform. */ +#ifndef PB_WITHOUT_64BIT PB_STATIC_ASSERT(sizeof(int64_t) == 2 * sizeof(int32_t), INT64_T_WRONG_SIZE) PB_STATIC_ASSERT(sizeof(uint64_t) == 2 * sizeof(uint32_t), UINT64_T_WRONG_SIZE) +#endif /* This structure is used for 'bytes' arrays. * It has the number of bytes in the beginning, and after that an array. @@ -525,6 +527,14 @@ struct pb_extension_s { PB_DATAOFFSET_ ## placement(message, field, prevfield), \ PB_LTYPE_MAP_ ## type, ptr) +/* Field description for repeated static fixed count fields.*/ +#define PB_REPEATED_FIXED_COUNT(tag, type, placement, message, field, prevfield, ptr) \ + {tag, PB_ATYPE_STATIC | PB_HTYPE_REPEATED | PB_LTYPE_MAP_ ## type, \ + PB_DATAOFFSET_ ## placement(message, field, prevfield), \ + 0, \ + pb_membersize(message, field[0]), \ + pb_arraysize(message, field), ptr} + /* Field description for oneof fields. This requires taking into account the * union name also, that's why a separate set of macros is needed. */ diff --git a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_decode.h b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_decode.h index a426bdd..398b24a 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_decode.h +++ b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_decode.h @@ -85,6 +85,18 @@ bool pb_decode_noinit(pb_istream_t *stream, const pb_field_t fields[], void *des */ bool pb_decode_delimited(pb_istream_t *stream, const pb_field_t fields[], void *dest_struct); +/* Same as pb_decode_delimited, except that it does not initialize the destination structure. + * See pb_decode_noinit + */ +bool pb_decode_delimited_noinit(pb_istream_t *stream, const pb_field_t fields[], void *dest_struct); + +/* Same as pb_decode, except allows the message to be terminated with a null byte. + * NOTE: Until nanopb-0.4.0, pb_decode() also allows null-termination. This behaviour + * is not supported in most other protobuf implementations, so pb_decode_delimited() + * is a better option for compatibility. + */ +bool pb_decode_nullterminated(pb_istream_t *stream, const pb_field_t fields[], void *dest_struct); + #ifdef PB_ENABLE_MALLOC /* Release any allocated pointer fields. If you use dynamic allocation, you should * call this for any successfully decoded message when you are done with it. If @@ -124,7 +136,11 @@ bool pb_skip_field(pb_istream_t *stream, pb_wire_type_t wire_type); /* Decode an integer in the varint format. This works for bool, enum, int32, * int64, uint32 and uint64 field types. */ +#ifndef PB_WITHOUT_64BIT bool pb_decode_varint(pb_istream_t *stream, uint64_t *dest); +#else +#define pb_decode_varint pb_decode_varint32 +#endif /* Decode an integer in the varint format. This works for bool, enum, int32, * and uint32 field types. */ @@ -132,15 +148,21 @@ bool pb_decode_varint32(pb_istream_t *stream, uint32_t *dest); /* Decode an integer in the zig-zagged svarint format. This works for sint32 * and sint64. */ +#ifndef PB_WITHOUT_64BIT bool pb_decode_svarint(pb_istream_t *stream, int64_t *dest); +#else +bool pb_decode_svarint(pb_istream_t *stream, int32_t *dest); +#endif /* Decode a fixed32, sfixed32 or float value. You need to pass a pointer to * a 4-byte wide C variable. */ bool pb_decode_fixed32(pb_istream_t *stream, void *dest); +#ifndef PB_WITHOUT_64BIT /* Decode a fixed64, sfixed64 or double value. You need to pass a pointer to * a 8-byte wide C variable. */ bool pb_decode_fixed64(pb_istream_t *stream, void *dest); +#endif /* Make a limited-length substream for reading a PB_WT_STRING field. */ bool pb_make_string_substream(pb_istream_t *stream, pb_istream_t *substream); diff --git a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_encode.h b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_encode.h index d9909fb..8bf78dd 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_encode.h +++ b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Headers/pb_encode.h @@ -71,6 +71,12 @@ bool pb_encode(pb_ostream_t *stream, const pb_field_t fields[], const void *src_ */ bool pb_encode_delimited(pb_ostream_t *stream, const pb_field_t fields[], const void *src_struct); +/* Same as pb_encode, but appends a null byte to the message for termination. + * NOTE: This behaviour is not supported in most other protobuf implementations, so pb_encode_delimited() + * is a better option for compatibility. + */ +bool pb_encode_nullterminated(pb_ostream_t *stream, const pb_field_t fields[], const void *src_struct); + /* Encode the message to get the size of the encoded data, but do not store * the data. */ bool pb_get_encoded_size(size_t *size, const pb_field_t fields[], const void *src_struct); @@ -123,11 +129,19 @@ bool pb_encode_tag(pb_ostream_t *stream, pb_wire_type_t wiretype, uint32_t field /* Encode an integer in the varint format. * This works for bool, enum, int32, int64, uint32 and uint64 field types. */ +#ifndef PB_WITHOUT_64BIT bool pb_encode_varint(pb_ostream_t *stream, uint64_t value); +#else +bool pb_encode_varint(pb_ostream_t *stream, uint32_t value); +#endif /* Encode an integer in the zig-zagged svarint format. * This works for sint32 and sint64. */ +#ifndef PB_WITHOUT_64BIT bool pb_encode_svarint(pb_ostream_t *stream, int64_t value); +#else +bool pb_encode_svarint(pb_ostream_t *stream, int32_t value); +#endif /* Encode a string or bytes type field. For strings, pass strlen(s) as size. */ bool pb_encode_string(pb_ostream_t *stream, const pb_byte_t *buffer, size_t size); @@ -136,9 +150,11 @@ bool pb_encode_string(pb_ostream_t *stream, const pb_byte_t *buffer, size_t size * You need to pass a pointer to a 4-byte wide C variable. */ bool pb_encode_fixed32(pb_ostream_t *stream, const void *value); +#ifndef PB_WITHOUT_64BIT /* Encode a fixed64, sfixed64 or double value. * You need to pass a pointer to a 8-byte wide C variable. */ bool pb_encode_fixed64(pb_ostream_t *stream, const void *value); +#endif /* Encode a submessage field. * You need to pass the pb_field_t array and pointer to struct, just like diff --git a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Modules/module.modulemap b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Modules/module.modulemap index 91c3a63..5d98024 100644 --- a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Modules/module.modulemap +++ b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/Modules/module.modulemap @@ -1,5 +1,5 @@ framework module nanopb { - umbrella header "nanopb.h" - export * - module * { export *} +umbrella header "nanopb.h" +export * +module * { export * } } diff --git a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/nanopb b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/nanopb index cb33db8..b2a02e8 100644 Binary files a/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/nanopb and b/WikiRaces/Shared/Frameworks/Analytics/nanopb.framework/nanopb differ diff --git a/WikiRaces/Shared/Logging/PlayerAnonymousMetrics.swift b/WikiRaces/Shared/Logging/PlayerAnonymousMetrics.swift new file mode 100644 index 0000000..3921b76 --- /dev/null +++ b/WikiRaces/Shared/Logging/PlayerAnonymousMetrics.swift @@ -0,0 +1,114 @@ +// +// PlayerMetrics.swift +// WikiRaces +// +// Created by Andrew Finke on 9/25/17. +// Copyright © 2017 Andrew Finke. All rights reserved. +// + +import UIKit +import WKRKit + +#if !MULTIWINDOWDEBUG +import Crashlytics +import FirebaseCore +#endif + +internal struct PlayerAnonymousMetrics { + + // MARK: - Logging Event Types + + enum CrashLogEvent { + case userAction(String) + case gameState(String) + case error(String) + } + + // MARK: - Analytic Event Types + + enum Event: String { + // Non Game + case leaderboard, versionInfo + case pressedJoin, pressedHost, pressedGlobalJoin, pressedLocalOptions + case namePromptResult, nameType + case cloudStatus, interfaceMode + + // Game All Players + case pageView, pageBlocked, pageError + case quitRace, forfeited, usedHelp, usedReload, fatalError, backupQuit + case openedHistory, openedHistorySF, openedShare, pressedReadyButton, voted + case finalVotes + case linkOnPage, missedLink, foundPage, pageLoadingError + + // Game Host + case hostStartedMatch, hostStartedRace, hostEndedRace + case hostCancelledPreMatch, hostStartMidMatchInviting + case hostStartedSoloMatch + case globalFailedToFindHost + + case mpcRaceCompleted, gkRaceCompleted, soloRaceCompleted + case banHammer + case connectionTestResult + case displayedMedals, puzzleViewScrolled + + case collectiveVotingArticlesSeen, localVotingArticlesSeen, localVotingArticlesReset + case votingArticleValidationFailure, votingArticlesWeightedTiebreak + + case automaticResultsImageSave + + //swiftlint:disable:next cyclomatic_complexity + init(event: WKRLogEvent) { + switch event.type { + case .linkOnPage: self = .linkOnPage + case .foundPage: self = .foundPage + case .pageBlocked: self = .pageBlocked + case .pageLoadingError: self = .pageLoadingError + case .pageView: self = .pageView + case .missedLink: self = .missedLink + + case .collectiveVotingArticlesSeen: self = .collectiveVotingArticlesSeen + case .localVotingArticlesSeen: self = .localVotingArticlesSeen + case .localVotingArticlesReset: self = .localVotingArticlesReset + case .votingArticleValidationFailure: self = .votingArticleValidationFailure + case .votingArticlesWeightedTiebreak: self = .votingArticlesWeightedTiebreak + } + } + } + + // MARK: - Logging Events + + public static func log(event: CrashLogEvent) { + #if MULTIWINDOWDEBUG + switch event { + case .userAction(let action): + print("UserAction: ", action) + case .gameState(let description): + print("GameState: ", description) + case .error(let error): + print("Error: ", error) + } + #else + switch event { + case .userAction(let action): + CLSNSLogv("UserAction: %@", getVaList([action])) + case .gameState(let description): + CLSNSLogv("GameState: %@", getVaList([description])) + case .error(let error): + CLSNSLogv("LoggedError: %@", getVaList([error])) + } + #endif + } + + // MARK: - Analytic Events + + public static func log(event: Event, attributes: [String: Any]? = nil) { + #if !MULTIWINDOWDEBUG && !DEBUG + Answers.logCustomEvent(withName: event.rawValue, customAttributes: attributes) + if !(attributes?.values.compactMap { $0 }.isEmpty ?? true) { + Analytics.logEvent(event.rawValue, parameters: attributes) + } else { + Analytics.logEvent(event.rawValue, parameters: nil) + } + #endif + } +} diff --git a/WikiRaces/Shared/Logging/PlayerDatabaseMetrics.swift b/WikiRaces/Shared/Logging/PlayerDatabaseMetrics.swift new file mode 100644 index 0000000..15abfb3 --- /dev/null +++ b/WikiRaces/Shared/Logging/PlayerDatabaseMetrics.swift @@ -0,0 +1,267 @@ +// +// PlayerDatabaseMetrics.swift +// WikiRaces +// +// Created by Andrew Finke on 1/29/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import CloudKit +import UIKit +import WKRKit + +class PlayerDatabaseMetrics: NSObject { + + // MARK: - Types + + private struct ProcessedResults { + let csvURL: URL + let playerCount: Int + let totalPlayerTime: Int + let links: Int + } + + static let banHammerNotification = Notification.Name("banHammerNotification") + + // MARK: - Properties + + static var shared = PlayerDatabaseMetrics() + + private let container = CKContainer.default() + private let publicDB = CKContainer.default().publicCloudDatabase + + private var userRecord: CKRecord? + private var userStatsRecord: CKRecord? + + private var isConnecting = false + private var isCreatingStatsRecord = false + private var isSyncing = false + + private var queuedKeyValues = [String: CKRecordValueProtocol]() + + // MARK: - Connecting + + func connect() { + #if MULTIWINDOWDEBUG + return + #endif + + guard !isConnecting else { return } + isConnecting = true + + container.fetchUserRecordID(completionHandler: { (userRecordID, _) in + guard let userRecordID = userRecordID else { + self.isConnecting = false + return + } + self.publicDB.fetch(withRecordID: userRecordID, completionHandler: { (userRecord, _) in + self.userRecord = userRecord + guard let userRecord = userRecord else { + self.isConnecting = false + return + } + + if let raceCount = userRecord["Races"] as? NSNumber, raceCount.intValue == -1 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { + let name = PlayerDatabaseMetrics.banHammerNotification + NotificationCenter.default.post(name: name, object: nil) + }) + return + } + + // Get user stats record, or create new one. + guard let statsRecordName = userRecord.object(forKey: "UserStatsNamev3") as? NSString, + statsRecordName.length > 5 else { + self.createUserStatsRecord() + self.isConnecting = false + return + } + let userStatsRecordID = CKRecord.ID(recordName: statsRecordName as String) + self.publicDB.fetch(withRecordID: userStatsRecordID, completionHandler: { (userStatsRecord, error) in + if let error = error as? CKError, error.code == CKError.unknownItem { + self.createUserStatsRecord() + self.isConnecting = false + return + } + guard let userStatsRecord = userStatsRecord else { return } + self.userStatsRecord = userStatsRecord + self.isConnecting = false + DispatchQueue.main.async { + self.saveKeyValues() + } + }) + }) + }) + } + + private func createUserStatsRecord() { + guard let userRecord = userRecord, !isCreatingStatsRecord else { return } + isCreatingStatsRecord = true + + let userStatsRecord = CKRecord(recordType: "UserStatsv3") + userStatsRecord["DeviceNames"] = [UIDevice.current.name] + publicDB.save(userStatsRecord, completionHandler: { (savedUserStatsRecord, _) in + guard let savedUserStatsRecord = savedUserStatsRecord else { + self.isCreatingStatsRecord = false + return + } + userRecord["UserStatsNamev3"] = savedUserStatsRecord.recordID.recordName as NSString + + self.publicDB.save(userRecord, completionHandler: { (savedUserRecord, _) in + self.userRecord = savedUserRecord + self.userStatsRecord = savedUserStatsRecord + self.isCreatingStatsRecord = false + DispatchQueue.main.async { + self.saveKeyValues() + } + }) + }) + } + + // MARK: - Events + + func log(value: CKRecordValueProtocol, for key: String) { + DispatchQueue.main.async { + self.queuedKeyValues[key] = value + DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { + self.saveKeyValues() + }) + } + } + + private func saveKeyValues() { + guard !queuedKeyValues.isEmpty, + !isConnecting, + !isCreatingStatsRecord, + !isSyncing, + let record = userStatsRecord else { return } + + isSyncing = true + let keyValues = queuedKeyValues + queuedKeyValues = [:] + + for (key, value) in keyValues { + if key == "GCAliases" || key == "DeviceNames" || key == "CustomNames" { + guard let name = value as? String else { return } + var names = [String]() + if let existingNames = record[key] as? [String] { + names.append(contentsOf: existingNames) + } + names.append(name) + record[key] = Array(Set(names)) + } else if let num = value as? Double { + if num.isFinite { + if floor(num) == num { + record[key] = Int(num) + } else { + record[key] = num + } + } + } else { + record[key] = value + } + } + + publicDB.save(record) { savedUserStatsRecord, _ in + if let savedUserStatsRecord = savedUserStatsRecord { + self.userStatsRecord = savedUserStatsRecord + } else { + self.userStatsRecord = nil + self.userRecord = nil + DispatchQueue.main.async { + for (key, value) in keyValues where self.queuedKeyValues[key] == nil { + self.queuedKeyValues[key] = value + } + } + self.connect() + } + DispatchQueue.main.async { + self.isSyncing = false + self.saveKeyValues() + } + } + } + + // MARK: - Results Collection + + func record(results: WKRResultsInfo) { + #if MULTIWINDOWDEBUG + return + #endif + + guard let processedResults = process(results: results) else { return } + + let resultsRecord = CKRecord(recordType: "RaceResult") + resultsRecord["CSV"] = CKAsset(fileURL: processedResults.csvURL) + resultsRecord["Links"] = NSNumber(value: processedResults.links) + resultsRecord["PlayerCount"] = NSNumber(value: processedResults.playerCount) + resultsRecord["TotalPlayerTime"] = NSNumber(value: processedResults.totalPlayerTime) + + publicDB.save(resultsRecord) { _, _ in + try? FileManager.default.removeItem(at: processedResults.csvURL) + } + } + + private func process(results: WKRResultsInfo) -> ProcessedResults? { + + func csvRow(for player: WKRPlayer, state: WKRPlayerState) -> String { + + func formatted(row: String?) -> String { + return row?.replacingOccurrences(of: ",", with: " ") ?? "" + } + + var string = "" + string += formatted(row: player.name) + "," + string += formatted(row: state.text) + "," + + if state == .foundPage { + let time = String(player.raceHistory?.duration ?? 0) + string += formatted(row: time) + "," + } else { + string += "," + } + + for entry in player.raceHistory?.entries ?? [] { + let title = (entry.page.title ?? "") + let duration = String(entry.duration ?? 0) + "|" + string += formatted(row: duration + title) + "," + } + + string.removeLast() + + return string + } + + var links = 0 + var totalPlayerTime = 0 + + var csvString = "Name,State,Duration,Pages\n" + for index in 0.. Double { + if self == .multiplayerAverage { + let races = PlayerStatsManager.shared.multiplayerRaces + let points = PlayerStatsManager.shared.multiplayerPoints + let value = points / races + return value.isNaN ? 0.0 : value + } else { + return UserDefaults.standard.double(forKey: key) + } + } + + func set(value: Double) { + UserDefaults.standard.set(value, forKey: key) + } + + func increment(by value: Double = 1) { + let newValue = self.value() + value + UserDefaults.standard.set(newValue, forKey: key) + } + +} diff --git a/WikiRaces/Shared/Logging/PlayerMetrics.swift b/WikiRaces/Shared/Logging/PlayerMetrics.swift deleted file mode 100644 index 50c6e33..0000000 --- a/WikiRaces/Shared/Logging/PlayerMetrics.swift +++ /dev/null @@ -1,270 +0,0 @@ -// -// PlayerMetrics.swift -// WikiRaces -// -// Created by Andrew Finke on 9/25/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import CloudKit -import UIKit -import WKRKit - -#if !MULTIWINDOWDEBUG -import Crashlytics -import FirebaseCore -#endif - -internal struct PlayerMetrics { - - // MARK: - Logging Event Types - - enum ViewState: String { - case didLoad - case willAppear - case didAppear - case willDisappear - case didDisappear - } - - enum CrashLogEvent { - case userAction(String) - case viewState(String) - case gameState(String) - } - - // MARK: - Analytic Event Types - - enum StatEvent { - case players(unique: Int, total: Int) - case hasGCAlias(String), hasDeviceName(String), hasCustomName(String) - case usingGCAlias(String), usingDeviceName(String), usingCustomName(String) - case updatedStats(points: Int, races: Int, totalTime: Int, fastestTime: Int, pages: Int, - soloTotalTime: Int, soloPages: Int, soloRaces: Int) - case buildInfo(version: String, build: String) - } - - enum Event: String { - // Non Game - case leaderboard, versionInfo - case pressedJoin, pressedHost - case namePromptResult, nameType - case cloudStatus, interfaceMode - - // Game All Players - case pageView, pageBlocked, pageError - case quitRace, forfeited, usedHelp, fatalError, backupQuit - case openedHistory, pressedReadyButton, voted - case finalVotes - - // Game Host - case hostStartedMatch, hostStartedRace, hostEndedRace - case hostCancelledPreMatch, hostStartMidMatchInviting - case hostStartedSoloMatch - } - - // MARK: - Results Collection Types - - struct ProcessedResults { - let csvURL: URL - let playerCount: Int - let totalPlayerTime: Int - let links: Int - } - - // MARK: - Logging Events - - public static func log(state: ViewState, for object: UIViewController) { - log(event: .viewState("\(type(of: object)): " + state.rawValue)) - } - - public static func log(presentingOf modal: UIViewController, on object: UIViewController) { - var titleString = "Title: " - if let title = modal.title { - titleString += title - } else { - titleString += "nil" - } - log(event: .viewState("\(type(of: object)): Presenting: \(type(of: modal)) " + titleString)) - } - - public static func log(event: CrashLogEvent) { - #if !MULTIWINDOWDEBUG - switch event { - case .userAction(let action): - CLSNSLogv("UserAction: %@", getVaList([action])) - case .viewState(let view): - CLSNSLogv("ViewState: %@", getVaList([view])) - case .gameState(let description): - CLSNSLogv("GameState: %@", getVaList([description])) - } - #endif - } - - // MARK: - Analytic Events - - public static func log(event: Event, attributes: [String: Any]? = nil) { - #if !MULTIWINDOWDEBUG && !DEBUG - Answers.logCustomEvent(withName: event.rawValue, customAttributes: attributes) - if !(attributes?.values.compactMap { $0 }.isEmpty ?? true) { - Analytics.logEvent(event.rawValue, parameters: attributes) - } else { - Analytics.logEvent(event.rawValue, parameters: nil) - } - #endif - } - - //swiftlint:disable:next cyclomatic_complexity function_body_length - public static func log(event: StatEvent) { - #if !MULTIWINDOWDEBUG - let container = CKContainer.default() - let publicDB = container.publicCloudDatabase - - // Fetch user record ID, then user record. - container.fetchUserRecordID(completionHandler: { (userRecordID, _) in - guard let userRecordID = userRecordID else { return } - publicDB.fetch(withRecordID: userRecordID, completionHandler: { (userRecord, _) in - guard let userRecord = userRecord else { return } - - // Get user stats record, or create new one. - let statsRecordName = userRecord.object(forKey: "UserStatsName") as? NSString ?? " " - let userStatsRecordID = CKRecord.ID(recordName: statsRecordName as String) - publicDB.fetch(withRecordID: userStatsRecordID, completionHandler: { (userStatsRecord, error) in - - var userStatsRecord = userStatsRecord - if let error = error as? CKError, error.code == CKError.unknownItem { - userStatsRecord = CKRecord(recordType: "UserStats") - } - guard let record = userStatsRecord else { return } - - // Update user stats record. - switch event { - case .usingGCAlias(let alias): - record["GCAlias"] = alias as NSString - log(event: .nameType, attributes: ["Type": "GCAlias"]) - case .usingDeviceName(let name): - record["DeviceName"] = name as NSString - log(event: .nameType, attributes: ["Type": "DeviceName"]) - case .usingCustomName(let name): - record["CustomName"] = name as NSString - log(event: .nameType, attributes: ["Type": "CustomName"]) - case .hasGCAlias(let alias): - record["GCAlias"] = alias as NSString - case .hasDeviceName(let name): - record["DeviceName"] = name as NSString - case .hasCustomName(let name): - record["CustomName"] = name as NSString - case .updatedStats(let points, let races, let totalTime, let fastestTime, let pages, - let soloTotalTime, let soloPages, let soloRaces): - record["Points"] = NSNumber(value: points) - record["Races"] = NSNumber(value: races) - record["TotalTime"] = NSNumber(value: totalTime) - record["FastestTime"] = NSNumber(value: fastestTime) - record["Pages"] = NSNumber(value: pages) - record["SoloTotalTime"] = NSNumber(value: soloTotalTime) - record["SoloPages"] = NSNumber(value: soloPages) - record["SoloRaces"] = NSNumber(value: soloRaces) - case .buildInfo(let version, let build): - record["BundleVersion"] = version as NSString - record["BundleBuild"] = build as NSString - case .players(let unique, let total): - record["TotalPlayers"] = NSNumber(value: total) - record["UniquePlayers"] = NSNumber(value: unique) - } - - // Save updated stats record and update user record with stats record ID. - publicDB.save(record, completionHandler: { (savedUserStatsRecord, _) in - guard let savedUserStatsRecord = savedUserStatsRecord else { return } - userRecord["UserStatsName"] = savedUserStatsRecord.recordID.recordName as NSString - publicDB.save(userRecord, completionHandler: { (_, _) in }) - }) - - }) - }) - }) - #endif - } - - // MARK: - Results Collection - - public static func record(results: WKRResultsInfo) { - #if MULTIWINDOWDEBUG - return - #endif - - guard let processedResults = process(results: results) else { return } - - let resultsRecord = CKRecord(recordType: "RaceResult") - resultsRecord["CSV"] = CKAsset(fileURL: processedResults.csvURL) - resultsRecord["Links"] = NSNumber(value: processedResults.links) - resultsRecord["PlayerCount"] = NSNumber(value: processedResults.playerCount) - resultsRecord["TotalPlayerTime"] = NSNumber(value: processedResults.totalPlayerTime) - - CKContainer.default().publicCloudDatabase.save(resultsRecord) { (_, _) in - try? FileManager.default.removeItem(at: processedResults.csvURL) - } - } - - private static func process(results: WKRResultsInfo) -> ProcessedResults? { - - func csvRow(for player: WKRPlayer, state: WKRPlayerState) -> String { - - func formatted(row: String?) -> String { - return row?.replacingOccurrences(of: ",", with: " ") ?? "" - } - - var string = "" - string += formatted(row: player.name) + "," - string += formatted(row: state.text) + "," - - if state == .foundPage { - let time = String(player.raceHistory?.duration ?? 0) - string += formatted(row: time) + "," - } else { - string += "," - } - - for entry in player.raceHistory?.entries ?? [] { - let title = (entry.page.title ?? "") - let duration = String(entry.duration ?? 0) + "|" - string += formatted(row: duration + title) + "," - } - - string.removeLast() - - return string - } - - var links = 0 - var totalPlayerTime = 0 - - var csvString = "Name,State,Duration,Pages\n" - for index in 0.. Void)? + + private let defaults = UserDefaults.standard + private let keyValueStore = NSUbiquitousKeyValueStore.default + + // MARK: - Computed Properties + + var multiplayerPoints: Double { + return PlayerDatabaseStat.mpcPoints.value() + PlayerDatabaseStat.gkPoints.value() + } + + var multiplayerRaces: Double { + return PlayerDatabaseStat.mpcRaces.value() + PlayerDatabaseStat.gkRaces.value() + } + + var multiplayerPages: Double { + return PlayerDatabaseStat.mpcPages.value() + PlayerDatabaseStat.gkPages.value() + } + + var multiplayerPixelsScrolled: Double { + return PlayerDatabaseStat.mpcPixelsScrolled.value() + PlayerDatabaseStat.gkPixelsScrolled.value() + } + + var multiplayerTotalTime: Double { + return PlayerDatabaseStat.mpcTotalTime.value() + PlayerDatabaseStat.gkTotalTime.value() + } + + var multiplayerFastestTime: Double { + let mpcTime = PlayerDatabaseStat.mpcFastestTime.value() + if mpcTime == 0 { + return PlayerDatabaseStat.gkFastestTime.value() + } else { + let gkTime = PlayerDatabaseStat.gkFastestTime.value() + if gkTime == 0 { + return mpcTime + } else if gkTime < mpcTime { + return gkTime + } else { + return mpcTime + } + } + } + + // MARK: - Initalization + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Helpers + + func start() { + ubiquitousStoreSync() + leaderboardSync() + + NotificationCenter.default.addObserver(self, + selector: #selector(keyValueStoreChanged(_:)), + name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, + object: keyValueStore) + + keyValueStore.synchronize() + playerDatabaseSync() + } + + // MARK: - Set/Get Stats + + func viewedPage(raceType: RaceType) { + var stat: PlayerDatabaseStat + switch raceType { + case .mpc: + stat = PlayerDatabaseStat.mpcPages + case .gameKit: + stat = PlayerDatabaseStat.gkPages + case .solo: + stat = PlayerDatabaseStat.soloPages + } + stat.increment() + } + + func connected(to players: [String], raceType: RaceType) { + var playersKey = "" + var uniqueStat = PlayerDatabaseStat.mpcUniquePlayers + var totalStat = PlayerDatabaseStat.mpcTotalPlayers + let matchStat: PlayerDatabaseStat + switch raceType { + case .mpc: + playersKey = "PlayersArray" + uniqueStat = PlayerDatabaseStat.mpcUniquePlayers + totalStat = PlayerDatabaseStat.mpcTotalPlayers + matchStat = .mpcMatch + case .gameKit: + playersKey = "GKPlayersArray" + uniqueStat = PlayerDatabaseStat.gkUniquePlayers + totalStat = PlayerDatabaseStat.gkTotalPlayers + matchStat = .gkMatch + case .solo: + matchStat = .soloMatch + } + + var existingPlayers = defaults.stringArray(forKey: playersKey) ?? [] + existingPlayers += players + defaults.set(existingPlayers, forKey: playersKey) + syncPlayerNamesStat(raceType: raceType) + + let uniquePlayers = Array(Set(existingPlayers)).count + let totalPlayers = existingPlayers.count + + defaults.set(uniquePlayers, forKey: uniqueStat.key) + defaults.set(totalPlayers, forKey: totalStat.key) + + matchStat.increment() + + logStatToMetric(matchStat) + logStatToMetric(.mpcUniquePlayers) + logStatToMetric(.mpcTotalPlayers) + logStatToMetric(.gkUniquePlayers) + logStatToMetric(.gkTotalPlayers) + + ubiquitousStoreSync() + } + + //swiftlint:disable:next function_body_length + func completedRace(type: RaceType, points: Int, place: Int?, timeRaced: Int, pixelsScrolled: Int) { + let pointsStat: PlayerDatabaseStat? + let racesStat: PlayerDatabaseStat + let totalTimeStat: PlayerDatabaseStat + let fastestTimeStat: PlayerDatabaseStat + let pixelsStat: PlayerDatabaseStat + + let finishFirstStat: PlayerDatabaseStat + let finishSecondStat: PlayerDatabaseStat? + let finishThirdStat: PlayerDatabaseStat? + let finishDNFStat: PlayerDatabaseStat + + switch type { + case .mpc: + pointsStat = .mpcPoints + racesStat = .mpcRaces + totalTimeStat = .mpcTotalTime + fastestTimeStat = .mpcFastestTime + pixelsStat = .mpcPixelsScrolled + + finishFirstStat = .mpcRaceFinishFirst + finishSecondStat = .mpcRaceFinishSecond + finishThirdStat = .mpcRaceFinishThird + finishDNFStat = .mpcRaceDNF + case .gameKit: + pointsStat = .gkPoints + racesStat = .gkRaces + totalTimeStat = .gkTotalTime + fastestTimeStat = .gkFastestTime + pixelsStat = .gkPixelsScrolled + + finishFirstStat = .gkRaceFinishFirst + finishSecondStat = .gkRaceFinishSecond + finishThirdStat = .gkRaceFinishThird + finishDNFStat = .gkRaceDNF + case .solo: + pointsStat = nil + racesStat = .soloRaces + totalTimeStat = .soloTotalTime + fastestTimeStat = .soloFastestTime + pixelsStat = .soloPixelsScrolled + + finishFirstStat = .soloRaceFinishFirst + finishSecondStat = nil + finishThirdStat = nil + finishDNFStat = .soloRaceDNF + } + + pointsStat?.increment(by: Double(points)) + racesStat.increment() + totalTimeStat.increment(by: Double(timeRaced)) + pixelsStat.increment(by: Double(pixelsScrolled)) + + if let place = place { + if place == 1 { + finishFirstStat.increment() + } else if place == 2 { + finishSecondStat?.increment() + } else if place == 3 { + finishThirdStat?.increment() + } + + let currentFastestTime = fastestTimeStat.value() + if currentFastestTime == 0 || timeRaced < Int(currentFastestTime) { + fastestTimeStat.set(value: Double(timeRaced)) + } + SKStoreReviewController.shouldPromptForRating = true + } else { + finishDNFStat.increment() + SKStoreReviewController.shouldPromptForRating = false + } + + ubiquitousStoreSync() + leaderboardSync() + playerDatabaseSync() + } + + // MARK: - Syncing + + @objc + private func keyValueStoreChanged(_ notification: NSNotification) { + guard let userInfo = notification.userInfo, + let changedKeys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String], + let reasonForChange = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? NSNumber else { + return + } + + let reason = reasonForChange.intValue + if reason == NSUbiquitousKeyValueStoreServerChange || reason == NSUbiquitousKeyValueStoreInitialSyncChange { + for key in changedKeys { + guard let stat = PlayerDatabaseStat(rawValue: key) else { return } + self.sync(stat, key: key) + } + } + + leaderboardSync() + playerDatabaseSync() + } + + private func sync(_ stat: PlayerDatabaseStat, key: String) { + if PlayerDatabaseStat.numericHighStats.contains(stat) { + let deviceValue = defaults.double(forKey: key) + let cloudValue = keyValueStore.double(forKey: key) + if deviceValue > cloudValue { + keyValueStore.set(deviceValue, forKey: key) + } else if cloudValue > deviceValue { + defaults.set(cloudValue, forKey: key) + } + } else if PlayerDatabaseStat.numericLowStats.contains(stat) { + let deviceValue = defaults.double(forKey: stat.key) + let cloudValue = keyValueStore.double(forKey: stat.key) + if cloudValue < deviceValue && cloudValue != 0.0 { + defaults.set(cloudValue, forKey: stat.key) + } else if deviceValue != 0.0 { + keyValueStore.set(deviceValue, forKey: stat.key) + } + } else if stat == .mpcTotalPlayers { + syncPlayerNamesStat(raceType: .mpc) + } else if stat == .gkTotalPlayers { + syncPlayerNamesStat(raceType: .gameKit) + } + } + + private func ubiquitousStoreSync() { + for stat in PlayerDatabaseStat.numericHighStats { + let deviceValue = defaults.double(forKey: stat.key) + let cloudValue = keyValueStore.double(forKey: stat.key) + if deviceValue > cloudValue { + keyValueStore.set(deviceValue, forKey: stat.key) + } else if cloudValue > deviceValue { + defaults.set(cloudValue, forKey: stat.key) + } + } + for stat in PlayerDatabaseStat.numericLowStats { + let deviceValue = defaults.double(forKey: stat.key) + let cloudValue = keyValueStore.double(forKey: stat.key) + if cloudValue < deviceValue && cloudValue != 0.0 { + defaults.set(cloudValue, forKey: stat.key) + } else if deviceValue != 0.0 { + keyValueStore.set(deviceValue, forKey: stat.key) + } + } + + syncPlayerNamesStat(raceType: .mpc) + syncPlayerNamesStat(raceType: .gameKit) + } + + private func syncPlayerNamesStat(raceType: RaceType) { + var stat = "" + if raceType == .mpc { + stat = "PlayersArray" + } else if raceType == .gameKit { + stat = "GKPlayersArray" + } else { + return + } + let deviceValue = defaults.array(forKey: stat) ?? [] + let cloudValue = keyValueStore.array(forKey: stat) ?? [] + if deviceValue.count < cloudValue.count { + defaults.set(cloudValue, forKey: stat) + } else if cloudValue.count < deviceValue.count { + keyValueStore.set(deviceValue, forKey: stat) + } + } + + private func logStatToMetric(_ stat: PlayerDatabaseStat) { + let metrics = PlayerDatabaseMetrics.shared + metrics.log(value: stat.value(), for: stat.rawValue) + } + + private func logAllStatsToMetric() { + Set(PlayerDatabaseStat.allCases).forEach { logStatToMetric($0) } + } + + private func playerDatabaseSync() { + logAllStatsToMetric() + menuStatsUpdated?(multiplayerPoints, + multiplayerRaces, + PlayerDatabaseStat.multiplayerAverage.value()) + } + + private func leaderboardSync() { + guard GKLocalPlayer.local.isAuthenticated else { + return + } + + let points = multiplayerPoints + let races = multiplayerRaces + let average = PlayerDatabaseStat.multiplayerAverage.value() + + let totalTime = multiplayerTotalTime + let fastestTime = multiplayerFastestTime + let pagesViewed = multiplayerPages + let pixelsScrolled = multiplayerPixelsScrolled + + let pointsScore = GKScore(leaderboardIdentifier: "com.andrewfinke.wikiraces.points") + pointsScore.value = Int64(points) + + let racesScore = GKScore(leaderboardIdentifier: "com.andrewfinke.wikiraces.races") + racesScore.value = Int64(races) + + let totalTimeScore = GKScore(leaderboardIdentifier: "com.andrewfinke.wikiraces.totaltime") + totalTimeScore.value = Int64(totalTime / 60) + + let pagesViewedScore = GKScore(leaderboardIdentifier: "com.andrewfinke.wikiraces.pages") + pagesViewedScore.value = Int64(pagesViewed) + + let pixelsScrolledScore = GKScore(leaderboardIdentifier: "com.andrewfinke.wikiraces.pixelsscrolled") + pixelsScrolledScore.value = Int64(pixelsScrolled) + + var scores = [ + pointsScore, + racesScore, + totalTimeScore, + totalTimeScore, + pagesViewedScore, + pixelsScrolledScore + ] + + if races >= 5 { + let averageScore = GKScore(leaderboardIdentifier: "com.andrewfinke.wikiraces.ppr") + averageScore.value = Int64(average * 1_000) + scores.append(averageScore) + } + if fastestTime > 0 { + let fastestTimeScore = GKScore(leaderboardIdentifier: "com.andrewfinke.wikiraces.fastesttime") + fastestTimeScore.value = Int64(fastestTime) + scores.append(fastestTimeScore) + } + GKScore.report(scores, withCompletionHandler: nil) + } +} diff --git a/WikiRaces/Shared/Logging/StateLogTableViewController.swift b/WikiRaces/Shared/Logging/StateLogTableViewController.swift deleted file mode 100644 index 533ba84..0000000 --- a/WikiRaces/Shared/Logging/StateLogTableViewController.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// StateLogTableViewController.swift -// WikiRaces -// -// Created by Andrew Finke on 12/27/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import UIKit - -internal class StateLogTableViewController: UITableViewController { - - // MARK: - View Life Cycle - - override func viewDidLoad() { - super.viewDidLoad() - PlayerMetrics.log(state: .didLoad, for: self) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - PlayerMetrics.log(state: .willAppear, for: self) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - PlayerMetrics.log(state: .didAppear, for: self) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - PlayerMetrics.log(state: .willDisappear, for: self) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - PlayerMetrics.log(state: .didDisappear, for: self) - } - -} diff --git a/WikiRaces/Shared/Logging/StateLogViewController.swift b/WikiRaces/Shared/Logging/StateLogViewController.swift deleted file mode 100644 index 611933e..0000000 --- a/WikiRaces/Shared/Logging/StateLogViewController.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// StateLogViewController.swift -// WikiRaces -// -// Created by Andrew Finke on 12/27/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import UIKit - -internal class StateLogViewController: UIViewController { - - // MARK: - View Life Cycle - - override func viewDidLoad() { - super.viewDidLoad() - PlayerMetrics.log(state: .didLoad, for: self) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - PlayerMetrics.log(state: .willAppear, for: self) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - PlayerMetrics.log(state: .didAppear, for: self) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - PlayerMetrics.log(state: .willDisappear, for: self) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - PlayerMetrics.log(state: .didDisappear, for: self) - } - -} diff --git a/WikiRaces/Shared/Logging/StatsHelper.swift b/WikiRaces/Shared/Logging/StatsHelper.swift deleted file mode 100644 index 27c857e..0000000 --- a/WikiRaces/Shared/Logging/StatsHelper.swift +++ /dev/null @@ -1,325 +0,0 @@ -// -// StatsHelper.swift -// WikiRaces -// -// Created by Andrew Finke on 8/31/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import CloudKit -import GameKit - -//swiftlint:disable:next type_body_length -internal class StatsHelper { - - // MARK: - Types - - enum Stat: String { - case points - case races - case average - - // minutes - case totalTime - // seconds - case fastestTime - - case pages - - case totalPlayers - case uniquePlayers - - case soloPages - case soloTotalTime - case soloRaces - - static var numericHighStats: [Stat] = [ - .points, - .races, - .average, - .totalTime, - .pages, - .soloPages, - .soloTotalTime, - .soloRaces - ] - - var key: String { - return "WKRStat-" + self.rawValue - } - - var leaderboard: String { - switch self { - case .points: return "com.andrewfinke.wikiraces.points" - case .races: return "com.andrewfinke.wikiraces.races" - case .average: return "com.andrewfinke.wikiraces.ppr" - case .totalTime: return "com.andrewfinke.wikiraces.totaltime" - case .fastestTime: return "com.andrewfinke.wikiraces.fastesttime" - case .pages: return "com.andrewfinke.wikiraces.pages" - case .totalPlayers, .uniquePlayers, .soloPages, .soloTotalTime, .soloRaces: fatalError() - } - } - } - - // MARK: - Properties - - static let shared = StatsHelper() - - var keyStatsUpdated: ((_ points: Double, _ races: Double, _ average: Double) -> Void)? - private let migrationKey = "WKR3StatMigrationComplete" - - private let defaults = UserDefaults.standard - private let keyValueStore = NSUbiquitousKeyValueStore.default - - // MARK: - Initalization - - init() { - attemptMigration() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: - Helpers - - func start() { - attemptMigration() - cloudSync() - leaderboardSync() - - NotificationCenter.default.addObserver(self, - selector: #selector(keyValueStoreChanged(_:)), - name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, - object: keyValueStore) - - keyValueStore.synchronize() - } - - func updateStatsClosure() { - let races = statValue(for: .races) - let points = statValue(for: .points) - - let totalTime = statValue(for: .totalTime) - let fastestTime = statValue(for: .fastestTime) - - let pages = statValue(for: .pages) - - let soloTotalTime = statValue(for: .soloTotalTime) - let soloPages = statValue(for: .soloPages) - let soloRaces = statValue(for: .soloRaces) - - keyStatsUpdated?(points, races, statValue(for: .average)) - PlayerMetrics.log(event: .updatedStats(points: Int(points), - races: Int(races), - totalTime: Int(totalTime), - fastestTime: Int(fastestTime), - pages: Int(pages), - soloTotalTime: Int(soloTotalTime), - soloPages: Int(soloPages), - soloRaces: Int(soloRaces))) - } - - // MARK: - Set/Get Stats - - func statValue(for stat: Stat) -> Double { - if stat == .average { - let value = statValue(for: .points) / statValue(for: .races) - return value.isNaN ? 0.0 : value - } else { - return defaults.double(forKey: stat.key) - } - } - - func viewedPage(isSolo: Bool) { - let stat: Stat = isSolo ? .soloPages : .pages - let newPages = statValue(for: stat) + 1 - defaults.set(newPages, forKey: stat.key) - } - - func connected(to players: [String]) { - var existingPlayers = defaults.array(forKey: "PlayersArray") as? [String] ?? [] - existingPlayers += players - defaults.set(existingPlayers, forKey: "PlayersArray") - syncPlayerNamesStat() - - let uniquePlayers = Array(Set(existingPlayers)).count - let totalPlayers = existingPlayers.count - PlayerMetrics.log(event: .players(unique: uniquePlayers, total: totalPlayers)) - - defaults.set(uniquePlayers, forKey: Stat.uniquePlayers.key) - defaults.set(totalPlayers, forKey: Stat.totalPlayers.key) - - cloudSync() - } - - func completedRace(points: Int, timeRaced: Int, isSolo: Bool) { - if isSolo { - let newSoloTotalTime = statValue(for: .soloTotalTime) + Double(timeRaced) - let newSoloRaces = statValue(for: .soloRaces) + 1 - defaults.set(newSoloTotalTime, forKey: Stat.soloTotalTime.key) - defaults.set(newSoloRaces, forKey: Stat.soloRaces.key) - defaults.set(true, forKey: "ShouldPromptForRating") - } else { - let newPoints = statValue(for: .points) + Double(points) - let newRaces = statValue(for: .races) + 1 - let newTotalTime = statValue(for: .totalTime) + Double(timeRaced) - - defaults.set(newPoints, forKey: Stat.points.key) - defaults.set(newRaces, forKey: Stat.races.key) - defaults.set(newTotalTime, forKey: Stat.totalTime.key) - - // If found page, check for fastest completion time - if points > 0 { - let currentFastestTime = statValue(for: .fastestTime) - if currentFastestTime == 0 { - defaults.set(timeRaced, forKey: Stat.fastestTime.key) - } else if timeRaced < Int(currentFastestTime) { - defaults.set(timeRaced, forKey: Stat.fastestTime.key) - } - defaults.set(true, forKey: "ShouldPromptForRating") - } else { - defaults.set(false, forKey: "ShouldPromptForRating") - } - } - - cloudSync() - leaderboardSync() - updateStatsClosure() - } - - private func attemptMigration() { - guard !defaults.bool(forKey: migrationKey) else { - return - } - - let oldPoints = Double(UserDefaults.standard.integer(forKey: "Points")) - let oldRaces = Double(UserDefaults.standard.integer(forKey: "Rounds")) - - defaults.set(oldPoints, forKey: Stat.points.key) - defaults.set(oldRaces, forKey: Stat.races.key) - defaults.set(true, forKey: migrationKey) - - cloudSync() - leaderboardSync() - updateStatsClosure() - } - - // MARK: - Syncing - - @objc - private func keyValueStoreChanged(_ notification: NSNotification) { - guard let userInfo = notification.userInfo, - let changedKeys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String], - let reasonForChange = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? NSNumber else { - return - } - - let reason = reasonForChange.intValue - if reason == NSUbiquitousKeyValueStoreServerChange || reason == NSUbiquitousKeyValueStoreInitialSyncChange { - for key in changedKeys { - // Not currently syncing unique players array - guard let stat = Stat(rawValue: key) else { return } - - if Stat.numericHighStats.contains(stat) { - let deviceValue = defaults.double(forKey: key) - let cloudValue = keyValueStore.double(forKey: key) - if deviceValue > cloudValue { - keyValueStore.set(deviceValue, forKey: key) - } else if cloudValue > deviceValue { - defaults.set(cloudValue, forKey: key) - } - } else if stat == .fastestTime { - syncFastestTimeStat() - } else if stat == .totalPlayers { - syncPlayerNamesStat() - } - } - } - - leaderboardSync() - updateStatsClosure() - } - - private func cloudSync() { - for stat in Stat.numericHighStats { - let deviceValue = defaults.double(forKey: stat.key) - let cloudValue = keyValueStore.double(forKey: stat.key) - if deviceValue > cloudValue { - keyValueStore.set(deviceValue, forKey: stat.key) - } else if cloudValue > deviceValue { - defaults.set(cloudValue, forKey: stat.key) - } - } - - syncFastestTimeStat() - syncPlayerNamesStat() - } - - private func syncFastestTimeStat() { - let stat = Stat.fastestTime - let deviceValue = defaults.double(forKey: stat.key) - let cloudValue = keyValueStore.double(forKey: stat.key) - if cloudValue < deviceValue && cloudValue != 0.0 { - defaults.set(cloudValue, forKey: stat.key) - } else if deviceValue != 0.0 { - keyValueStore.set(deviceValue, forKey: stat.key) - } - } - - private func syncPlayerNamesStat() { - let stat = "PlayersArray" - let deviceValue = defaults.array(forKey: stat) ?? [] - let cloudValue = keyValueStore.array(forKey: stat) ?? [] - if deviceValue.count < cloudValue.count { - defaults.set(cloudValue, forKey: stat) - } else if cloudValue.count < deviceValue.count { - keyValueStore.set(deviceValue, forKey: stat) - } - } - - private func leaderboardSync() { - guard GKLocalPlayer.local.isAuthenticated else { - return - } - - let points = statValue(for: .points) - let races = statValue(for: .races) - let average = statValue(for: .average) - - let totalTime = statValue(for: .totalTime) - let fastestTime = statValue(for: .fastestTime) - - let pagesViewed = statValue(for: .pages) - - let pointsScore = GKScore(leaderboardIdentifier: Stat.points.leaderboard) - pointsScore.value = Int64(points) - - let racesScore = GKScore(leaderboardIdentifier: Stat.races.leaderboard) - racesScore.value = Int64(races) - - let totalTimeScore = GKScore(leaderboardIdentifier: Stat.totalTime.leaderboard) - totalTimeScore.value = Int64(totalTime / 60) - - let pagesViewedScore = GKScore(leaderboardIdentifier: Stat.pages.leaderboard) - pagesViewedScore.value = Int64(pagesViewed) - - // Waiting to see what these stats look like - // let playersRacedScore = GKScore(leaderboardIdentifier: Stat.uniquePlayers.leaderboard) - // playersRacedScore.value = Int64(playersRaced) - - var scores = [pointsScore, racesScore, totalTimeScore, totalTimeScore, pagesViewedScore] - if races >= 5 { - let averageScore = GKScore(leaderboardIdentifier: Stat.average.leaderboard) - averageScore.value = Int64(average * 1_000) - scores.append(averageScore) - } - if fastestTime > 0 { - let fastestTimeScore = GKScore(leaderboardIdentifier: Stat.fastestTime.leaderboard) - fastestTimeScore.value = Int64(fastestTime) - scores.append(fastestTimeScore) - } - GKScore.report(scores, withCompletionHandler: nil) - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/ConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/ConnectViewController.swift new file mode 100644 index 0000000..1c5e91f --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/ConnectViewController.swift @@ -0,0 +1,198 @@ +// +// ConnectViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 1/26/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import WKRKit + +class ConnectViewController: UIViewController { + + // MARK: - Types + + struct StartMessage: Codable { + let hostName: String + } + + // MARK: - Interface Elements + + /// General status label + let descriptionLabel = UILabel() + /// Activity spinner + let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) + /// The button to cancel joining/creating a race + let cancelButton = UIButton() + + var isFirstAppear = true + var isShowingMatch = false + var onQuit: (() -> Void)? + + // MARK: - Connection + + func runConnectionTest(completion: @escaping (Bool) -> Void) { + #if !MULTIWINDOWDEBUG + let trace = Performance.startTrace(name: "Connection Test Trace") + #endif + + let startDate = Date() + WKRConnectionTester.start { success in + DispatchQueue.main.async { + if success { + #if !MULTIWINDOWDEBUG + trace?.stop() + #endif + } + PlayerAnonymousMetrics.log(event: .connectionTestResult, + attributes: [ + "Result": NSNumber(value: success).intValue, + "Duration": -startDate.timeIntervalSinceNow + ]) + completion(success) + } + } + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + WKRSeenFinalArticlesStore.resetRemotePlayersSeenFinalArticles() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + descriptionLabel.alpha = 0.0 + activityIndicatorView.alpha = 0.0 + cancelButton.alpha = 0.0 + } + + // MARK: - Core Interface + + func setupCoreInterface() { + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium) + cancelButton.setTitleColor(.wkrTextColor, for: .normal) + cancelButton.alpha = 0.0 + cancelButton.setAttributedTitle(NSAttributedString(string: "CANCEL", spacing: 1.5), for: .normal) + cancelButton.addTarget(self, action: #selector(pressedCancelButton), for: .touchUpInside) + view.addSubview(cancelButton) + + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.alpha = 0.0 + descriptionLabel.textColor = .wkrTextColor + descriptionLabel.textAlignment = .center + descriptionLabel.font = UIFont.systemFont(ofSize: 17, weight: .medium) + view.addSubview(descriptionLabel) + updateDescriptionLabel(to: "CHECKING CONNECTION") + + activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + activityIndicatorView.alpha = 0.0 + activityIndicatorView.color = UIColor.wkrActivityIndicatorColor + activityIndicatorView.startAnimating() + view.addSubview(activityIndicatorView) + + let constraints = [ + descriptionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100), + descriptionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + activityIndicatorView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 30), + activityIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + cancelButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50), + cancelButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ] + NSLayoutConstraint.activate(constraints) + + view.backgroundColor = UIColor.wkrBackgroundColor + } + + func toggleCoreInterface(isHidden: Bool, + duration: TimeInterval, + and items: [UIView] = [], + completion: (() -> Void)? = nil) { + let views = [descriptionLabel, activityIndicatorView, cancelButton] + items + UIView.animate(withDuration: duration, + animations: { + views.forEach({ $0.alpha = isHidden ? 0 : 1}) + }, completion: { _ in + completion?() + }) + } + + // MARK: - Interface Updates + + func updateDescriptionLabel(to text: String) { + descriptionLabel.attributedText = NSAttributedString(string: text.uppercased(), + spacing: 2.0, + font: UIFont.systemFont(ofSize: 20.0, weight: .semibold)) + } + + func showConnectionSpeedError() { + showError(title: "Slow Connection", + message: "A fast internet connection is required to play WikiRaces.") + } + + /// Shows an error with a title + /// + /// - Parameters: + /// - title: The title of the error message + /// - message: The message body of the error + @objc + func showError(title: String, message: String, showSettingsButton: Bool = false) { + onQuit?() + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let action = UIAlertAction(title: "Menu", style: .default) { _ in + self.pressedCancelButton() + } + alertController.addAction(action) + + if showSettingsButton { + let settingsAction = UIAlertAction(title: "Open Settings", style: .default, handler: { _ in + PlayerAnonymousMetrics.log(event: .userAction("showError:settings")) + guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { + fatalError("Settings URL nil") + } + UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) + self.pressedCancelButton() + }) + alertController.addAction(settingsAction) + } + + present(alertController, animated: true, completion: nil) + } + + /// Cancels the join/create a race action and sends player back to main menu + @objc func pressedCancelButton() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + onQuit?() + + UIView.animate(withDuration: 0.25, animations: { + self.view.alpha = 0.0 + }, completion: { _ in + self.navigationController?.popToRootViewController(animated: false) + }) + } + + func showMatch(for networkConfig: WKRPeerNetworkConfig, + andHide views: [UIView]) { + + guard !isShowingMatch else { return } + isShowingMatch = true + + DispatchQueue.main.async { + self.toggleCoreInterface(isHidden: true, + duration: 0.25, + and: views, + completion: { + let controller = GameViewController() + controller.networkConfig = networkConfig + let nav = UINavigationController(rootViewController: controller) + nav.modalTransitionStyle = .crossDissolve + self.present(nav, animated: true, completion: nil) + }) + } + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController+Match.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController+Match.swift new file mode 100644 index 0000000..e80ca7a --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController+Match.swift @@ -0,0 +1,143 @@ +// +// GameKitConnectViewController+Match.swift +// WikiRaces +// +// Created by Andrew Finke on 1/26/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import GameKit +import WKRKit + +extension GameKitConnectViewController: GKMatchDelegate, GKMatchmakerViewControllerDelegate { + + // MARK: - Helpers + + func findMatch() { + let request = GKMatchRequest() + request.minPlayers = 2 + request.defaultNumberOfPlayers = 2 + let maxPlayerCount = min(WKRKitConstants.current.maxGlobalRacePlayers, + GKMatchRequest.maxPlayersAllowedForMatch(of: .peerToPeer)) + request.maxPlayers = maxPlayerCount + if let invite = GlobalRaceHelper.shared.lastInvite, + let controller = GKMatchmakerViewController(invite: invite) { + controller.matchmakerDelegate = self + present(controller, animated: true, completion: nil) + GlobalRaceHelper.shared.lastInvite = nil + } else if let controller = GKMatchmakerViewController(matchRequest: request) { + controller.matchmakerDelegate = self + present(controller, animated: true, completion: nil) + #if !MULTIWINDOWDEBUG + findTrace = Performance.startTrace(name: "Global Race Find Trace") + #endif + } else { + showError(title: "Unable To Find Match", message: "Please try again later.") + let info = "findMatch: No valid controller" + PlayerAnonymousMetrics.log(event: .error(info)) + } + } + + func sendStartMessageToPlayers() { + func fail() { + showError(title: "Unable To Start Match", + message: "Please try again later.") + } + guard let match = match else { + fail() + let info = "findMatch: No valid match" + PlayerAnonymousMetrics.log(event: .error(info)) + return + } + + let message = StartMessage(hostName: GKLocalPlayer.local.alias) + do { + let data = try JSONEncoder().encode(message) + try match.sendData(toAllPlayers: data, with: .reliable) + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + self.showMatch(for: .gameKit(match: match, + isHost: true), + andHide: []) + } + } catch { + fail() + let info = "sendStartMessageToPlayers: " + error.localizedDescription + PlayerAnonymousMetrics.log(event: .error(info)) + } + } + + // MARK: - GKMatchDelegate + + func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) { + if isPlayerHost, WKRSeenFinalArticlesStore.isRemoteTransferData(data) { + WKRSeenFinalArticlesStore.addRemoteTransferData(data) + } else if let object = try? JSONDecoder().decode(StartMessage.self, from: data) { + guard let hostAlias = self.hostPlayerAlias, object.hostName == hostAlias else { + PlayerAnonymousMetrics.log(event: .globalFailedToFindHost) + let message = "Please try again later." + showError(title: "Unable To Find Best Host", message: message) + return + } + if let data = WKRSeenFinalArticlesStore.encodedLocalPlayerSeenFinalArticles() { + try? match.send(data, to: [player], dataMode: .reliable) + } + showMatch(for: .gameKit(match: match, + isHost: isPlayerHost), + andHide: []) + } + } + + // MARK: - GKMatchmakerViewControllerDelegate + + func matchmakerViewControllerWasCancelled(_ viewController: GKMatchmakerViewController) { + dismiss(animated: true) { + self.pressedCancelButton() + } + } + + func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFailWithError error: Error) { + let info = "matchmaker...didFailWithError: " + error.localizedDescription + PlayerAnonymousMetrics.log(event: .error(info)) + + dismiss(animated: true) { + self.showError(title: "Unable To Find Match", + message: "Please try again later.") + } + } + + func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFind match: GKMatch) { + #if !MULTIWINDOWDEBUG + findTrace?.stop() + #endif + updateDescriptionLabel(to: "Finding best host") + + dismiss(animated: true) { + self.toggleCoreInterface(isHidden: false, duration: 0.25) + } + + match.delegate = self + self.match = match + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + var players = match.players + players.append(GKLocalPlayer.local) + if let hostPlayer = players.sorted(by: { $0.playerID > $1.playerID }).first { + self.hostPlayerAlias = hostPlayer.alias + if hostPlayer.playerID == GKLocalPlayer.local.playerID { + self.isPlayerHost = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { + self.sendStartMessageToPlayers() + }) + } + } else { + let info = "matchmaker...didFind: No host player" + PlayerAnonymousMetrics.log(event: .error(info)) + PlayerAnonymousMetrics.log(event: .globalFailedToFindHost) + self.showError(title: "Unable To Find Best Host", + message: "Please try again later.") + } + } + + GlobalRaceHelper.shared.lastInvite = nil + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController.swift new file mode 100644 index 0000000..5585c13 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/GameKit/GameKitConnectViewController.swift @@ -0,0 +1,69 @@ +// +// GameKitMatchmakingViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 1/25/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import GameKit + +import WKRKit + +#if !MULTIWINDOWDEBUG +import FirebasePerformance +#endif + +class GameKitConnectViewController: ConnectViewController { + + // MARK: - Properties + + var isPlayerHost = false + var hostPlayerAlias: String? + var match: GKMatch? + + #if !MULTIWINDOWDEBUG + var findTrace: Trace? + #endif + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + setupCoreInterface() + + onQuit = { [weak self] in + self?.match?.delegate = nil + self?.match?.disconnect() + } + + #if !MULTIWINDOWDEBUG + let playerName = GKLocalPlayer.local.alias + Crashlytics.sharedInstance().setUserName(playerName) + Analytics.setUserProperty(playerName, forName: "playerName") + #endif + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + guard isFirstAppear else { + return + } + isFirstAppear = false + + runConnectionTest { [weak self] success in + guard let self = self else { return } + if success { + self.toggleCoreInterface(isHidden: true, duration: 0.25) + self.findMatch() + } else if !success { + self.showConnectionSpeedError() + } + } + + toggleCoreInterface(isHidden: false, duration: 0.5) + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+Invite.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+Invite.swift similarity index 56% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+Invite.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+Invite.swift index 7f0d344..6ed17a7 100644 --- a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+Invite.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+Invite.swift @@ -9,6 +9,8 @@ import MultipeerConnectivity import UIKit +import WKRKit + #if !MULTIWINDOWDEBUG import FirebasePerformance #endif @@ -18,19 +20,49 @@ extension MPCConnectViewController: MCNearbyServiceAdvertiserDelegate, MCSession // MARK: - MCSessionDelegate func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { - session.delegate = nil - showMatch(isPlayerHost: false) + func start() { + session.delegate = nil + showMatch(for: .mpc(serviceType: serviceType, + session: session, + isHost: isPlayerHost), + andHide: [inviteView]) + } + + // 1. host context is nil when the invite was from a legacy app version (<= 3.6.2) + // 2. Otherwise, make sure that the host sent the start message + if hostContext == nil { + start() + } else if let object = try? JSONDecoder().decode(StartMessage.self, from: data) { + guard let hostName = hostPeerID?.displayName, object.hostName == hostName else { + let info = "session...didReceive: \(String(describing: hostPeerID?.displayName)), \(object.hostName)" + PlayerAnonymousMetrics.log(event: .error(info)) + + showError(title: "Connection Issue", + message: "The connection to the host was lost.") + return + } + start() + } } func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { DispatchQueue.main.async { if state == .connected && peerID == self.hostPeerID { self.updateDescriptionLabel(to: "WAITING FOR HOST") + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.25, execute: { + if let data = WKRSeenFinalArticlesStore.encodedLocalPlayerSeenFinalArticles() { + try? session.send(data, toPeers: [peerID], with: .reliable) + } + }) + #if !MULTIWINDOWDEBUG self.connectingTrace?.stop() self.connectingTrace = nil #endif } else if state == .notConnected && peerID == self.hostPeerID { + let info = "session...didChange: Host not connected" + PlayerAnonymousMetrics.log(event: .error(info)) self.showError(title: "Connection Issue", message: "The connection to the host was lost.") } } @@ -69,7 +101,12 @@ extension MPCConnectViewController: MCNearbyServiceAdvertiserDelegate, MCSession withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { - invites.append((invitationHandler, peerID)) + var hostContext: MPCHostContext? + if let data = context, + let object = try? JSONDecoder().decode(MPCHostContext.self, from: data) { + hostContext = object + } + invites.append((invitationHandler, peerID, hostContext)) showNextInvite() } @@ -85,13 +122,34 @@ extension MPCConnectViewController: MCNearbyServiceAdvertiserDelegate, MCSession let invite = invites.removeFirst() activeInvite = invite.handler + hostContext = invite.context hostPeerID = invite.host hostNameLabel.text = "FROM " + invite.host.displayName.uppercased() UIView.animate(withDuration: 0.25, animations: { + self.activityIndicatorView.alpha = 0.0 self.inviteView.alpha = 1.0 }) updateDescriptionLabel(to: "INVITE RECEIVED") + + activeInviteTimeoutTimer?.invalidate() + let timeout: TimeInterval = invite.context?.inviteTimeout ?? 10.0 + activeInviteTimeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, + repeats: false, + block: { [weak self] _ in + self?.declineInvite() + }) + + // Previous versions didn't send a host context object + guard let context = invite.context else { return } + if context.minPeerAppBuild > Bundle.main.appInfo.build { + let info = "showNextInvite: \(context.minPeerAppBuild) > \(Bundle.main.appInfo.build)" + PlayerAnonymousMetrics.log(event: .error(info)) + + //swiftlint:disable:next line_length + let message = "You received an invite to a race that requires the latest version of WikiRaces. Please download the update on the App Store." + showError(title: "Update Required", message: message) + } } // MARK: - User Actions @@ -99,18 +157,21 @@ extension MPCConnectViewController: MCNearbyServiceAdvertiserDelegate, MCSession /// Accepts the displayed invite @objc func acceptInvite() { - PlayerMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .userAction(#function)) + activeInviteTimeoutTimer?.invalidate() activeInvite?(true, session) - advertiser?.stopAdvertisingPeer() updateDescriptionLabel(to: "CONNECTING TO HOST") isShowingInvite = false UIView.animate(withDuration: 0.5) { + self.activityIndicatorView.alpha = 1.0 self.inviteView.alpha = 0.0 } + stopAdvertising() + #if !MULTIWINDOWDEBUG connectingTrace = Performance.startTrace(name: "Player Connecting Trace") #endif @@ -119,9 +180,10 @@ extension MPCConnectViewController: MCNearbyServiceAdvertiserDelegate, MCSession /// Declines the displayed invite @objc func declineInvite() { - PlayerMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .userAction(#function)) activeInvite?(false, session) + updateDescriptionLabel(to: "WAITING FOR INVITE") UIView.animate(withDuration: 0.25, animations: { diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+KB.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+KB.swift similarity index 100% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+KB.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+KB.swift diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+UI.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+UI.swift similarity index 83% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+UI.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+UI.swift index 062de2b..713edd0 100644 --- a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController+UI.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController+UI.swift @@ -12,17 +12,7 @@ extension MPCConnectViewController { // MARK: - Interface - func setupInterface() { - cancelButton.alpha = 0.0 - cancelButton.setAttributedTitle(NSAttributedString(string: "CANCEL", spacing: 1.5), for: .normal) - - updateDescriptionLabel(to: "CHECKING CONNECTION") - descriptionLabel.alpha = 0.0 - - activityIndicatorView.alpha = 0.0 - activityIndicatorView.color = UIColor.wkrActivityIndicatorColor - view.backgroundColor = UIColor.wkrBackgroundColor - + func setupInviteInterface() { inviteView.alpha = 0.0 inviteView.backgroundColor = UIColor.wkrBackgroundColor inviteView.translatesAutoresizingMaskIntoConstraints = false @@ -32,20 +22,20 @@ extension MPCConnectViewController { hostNameLabel.numberOfLines = 0 hostNameLabel.textColor = UIColor.wkrLightTextColor hostNameLabel.textAlignment = .center - hostNameLabel.font = UIFont.systemFont(ofSize: 16, weight: .regular) + hostNameLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium) hostNameLabel.translatesAutoresizingMaskIntoConstraints = false inviteView.addSubview(hostNameLabel) acceptButton.setTitle("Accept", for: .normal) acceptButton.setTitleColor(UIColor(red: 0, green: 122.0/255.0, blue: 1.0, alpha: 1.0), for: .normal) - acceptButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .regular) + acceptButton.titleLabel?.font = UIFont.systemFont(ofSize: 19, weight: .medium) acceptButton.translatesAutoresizingMaskIntoConstraints = false acceptButton.addTarget(self, action: #selector(acceptInvite), for: .touchUpInside) inviteView.addSubview(acceptButton) declineButton.setTitle("Decline", for: .normal) declineButton.setTitleColor(UIColor(red: 1, green: 0, blue: 0, alpha: 1.0), for: .normal) - declineButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .regular) + declineButton.titleLabel?.font = UIFont.systemFont(ofSize: 19, weight: .medium) declineButton.translatesAutoresizingMaskIntoConstraints = false declineButton.addTarget(self, action: #selector(declineInvite), for: .touchUpInside) inviteView.addSubview(declineButton) diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController.swift new file mode 100644 index 0000000..e64d188 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCConnectViewController/MPCConnectViewController.swift @@ -0,0 +1,196 @@ +// +// MPCConnectViewController.swift +// WikiRaces +// +// Created by Andrew Finke on 9/6/17. +// Copyright © 2017 Andrew Finke. All rights reserved. +// + +import GameKit +import MultipeerConnectivity +import UIKit + +import WKRKit +import WKRUIKit + +#if !MULTIWINDOWDEBUG +import FirebasePerformance +#endif + +internal class MPCConnectViewController: ConnectViewController { + + // MARK: - Interface Elements + + let inviteView = UIView() + let hostNameLabel = UILabel() + let acceptButton = UIButton() + let declineButton = UIButton() + + // MARK: - Properties + + var playerName = UIDevice.current.name + var isValidPlayerName = false + var isSolo = false + var isPlayerHost = false + var isShowingInvite = false + + // MARK: - MPC Properties + + var advertiser: MCNearbyServiceAdvertiser? + var activeInvite: ((Bool, MCSession) -> Void)? + var activeInviteTimeoutTimer: Timer? + + var invites = [(handler: ((Bool, MCSession) -> Void)?, host: MCPeerID, context: MPCHostContext?)]() + + var peerID: MCPeerID! + var hostPeerID: MCPeerID? + var hostContext: MPCHostContext? + + let serviceType = "WKRPeer30" + lazy var session: MCSession = { + return MCSession(peer: self.peerID) + }() + + #if !MULTIWINDOWDEBUG + var connectingTrace: Trace? + #endif + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Gets either the player name specified in settings.app, then GK alias, the device name + if let name = UserDefaults.standard.object(forKey: "name_preference") as? String { + playerName = name + PlayerAnonymousMetrics.log(event: .nameType, attributes: ["Type": "CustomName"]) + } else if GKLocalPlayer.local.isAuthenticated { + playerName = GKLocalPlayer.local.alias + PlayerAnonymousMetrics.log(event: .nameType, attributes: ["Type": "GCAlias"]) + } else { + PlayerAnonymousMetrics.log(event: .nameType, attributes: ["Type": "DeviceName"]) + } + + #if !MULTIWINDOWDEBUG + Crashlytics.sharedInstance().setUserName(playerName) + Analytics.setUserProperty(playerName, forName: "playerName") + #endif + + PlayerAnonymousMetrics.log(event: .userAction("Using player name \(playerName)")) + isValidPlayerName = playerName.utf8.count > 0 && playerName.utf8.count < 40 + guard isValidPlayerName else { return } + + // Uses existing peer ID object if already created (recommended per Apple docs) + if let pastPeerIDData = UserDefaults.standard.data(forKey: "PeerID"), + let lastPeerID = NSKeyedUnarchiver.unarchiveObject(with: pastPeerIDData) as? MCPeerID, + lastPeerID.displayName == playerName { + peerID = lastPeerID + } else { + // Attempting to prevent https://github.com/atfinke/WikiRaces/issues/43 + // Also, see rdar://47570877 + UserDefaults.standard.set(true, forKey: "AttemptingMCPeerIDCreation") + peerID = MCPeerID(displayName: playerName) + UserDefaults.standard.set(false, forKey: "AttemptingMCPeerIDCreation") + if let peerID = peerID { + let data = NSKeyedArchiver.archivedData(withRootObject: peerID) + UserDefaults.standard.set(data, forKey: "PeerID") + } + } + + setupCoreInterface() + setupInviteInterface() + + onQuit = { [weak self] in + self?.session.delegate = nil + self?.session.disconnect() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + stopAdvertising() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + guard isFirstAppear else { + return + } + isFirstAppear = false + + // Test the connection to Wikipedia + runConnectionTest { [weak self] success in + guard let self = self else { return } + if success && self.isValidPlayerName { + if self.isPlayerHost { + self.toggleCoreInterface(isHidden: true, + duration: 0.25, + and: [self.inviteView], + completion: { + self.presentHostInterface() + }) + } else { + self.startAdvertising() + } + } else if !success { + self.showConnectionSpeedError() + } + } + + toggleCoreInterface(isHidden: false, duration: 0.5) + + if !isValidPlayerName { + let info = "viewDidAppear...isValidPlayerName: " + playerName + PlayerAnonymousMetrics.log(event: .error(info)) + + let length = playerName.count == 0 ? "Short" : "Long" + let message = "Your player name is too \(length.lowercased()). " + showError(title: "Player Name Too \(length)", + message: message + "Would you like to open settings to adjust it?", + showSettingsButton: true) + } + } + + // MARK: - State Changes + + func stopAdvertising() { + advertiser?.stopAdvertisingPeer() + + // Reject all the pending invites + for invite in invites { + invite.handler?(false, session) + } + } + + func presentHostInterface() { + let controller = MPCHostViewController(style: .grouped) + controller.peerID = peerID + controller.session = session + controller.serviceType = serviceType + controller.listenerUpdate = { [weak self] update in + guard let self = self else { return } + switch update { + case .startMatch(let isSolo): + self.isSolo = isSolo + self.dismiss(animated: true, completion: { + var networkConfig: WKRPeerNetworkConfig = .solo(name: self.playerName) + if !isSolo { + networkConfig = .mpc(serviceType: self.serviceType, + session: self.session, + isHost: self.isPlayerHost) + } + self.showMatch(for: networkConfig, andHide: []) + }) + case .cancel: + self.dismiss(animated: true, completion: { + self.navigationController?.popToRootViewController(animated: false) + }) + } + } + + let nav = UINavigationController(rootViewController: controller) + present(nav, animated: true, completion: nil) + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift new file mode 100644 index 0000000..cea36b0 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift @@ -0,0 +1,18 @@ +// +// MPCHostContext.swift +// WikiRaces +// +// Created by Andrew Finke on 1/30/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import Foundation + +struct MPCHostContext: Codable { + let appBuild: Int + let appVersion: String + let name: String + + let inviteTimeout: TimeInterval + let minPeerAppBuild: Int +} diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostPeerStateCell.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostPeerStateCell.swift similarity index 100% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostPeerStateCell.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostPeerStateCell.swift diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostSearchingCell.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSearchingCell.swift similarity index 58% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostSearchingCell.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSearchingCell.swift index 611ab97..53d5d54 100644 --- a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostSearchingCell.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSearchingCell.swift @@ -19,15 +19,25 @@ internal class MPCHostSearchingCell: UITableViewCell { // MARK: - Initialization - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + isUserInteractionEnabled = false + backgroundColor = UIColor.wkrBackgroundColor + textLabel?.textColor = UIColor(red: 184.0 / 255.0, + green: 184.0 / 255.0, + blue: 184.0 / 255.0, + alpha: 1.0) updateText() - timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in - self.updateText() + timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + self?.updateText() }) } + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + // MARK: - Helpers func updateText() { diff --git a/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSoloCell.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSoloCell.swift new file mode 100644 index 0000000..adc4e57 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/Cells/MPCHostSoloCell.swift @@ -0,0 +1,33 @@ +// +// MPCHostSoloCell.swift +// WikiRaces +// +// Created by Andrew Finke on 2/25/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit + +internal class MPCHostSoloCell: UITableViewCell { + + // MARK: - Properties + + static let reuseIdentifier = "soloCell" + + // MARK: - Initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + backgroundColor = UIColor.wkrBackgroundColor + textLabel?.text = "Solo Race" + textLabel?.textColor = UIColor(red: 0, + green: 122.0 / 255.0, + blue: 1, + alpha: 1.0) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController+KB.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController+KB.swift similarity index 100% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController+KB.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController+KB.swift diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController+Table.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController+Table.swift similarity index 64% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController+Table.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController+Table.swift index 3cfffcd..407ca45 100644 --- a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController+Table.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController+Table.swift @@ -7,6 +7,8 @@ // import UIKit +import MultipeerConnectivity +import WKRKit extension MPCHostViewController { @@ -38,10 +40,8 @@ extension MPCHostViewController { override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { if section == 0 { - return """ - Make sure all players are on the same Wi-Fi network - and have Bluetooth enabled for the best results. - """ + //swiftlint:disable:next line_length + return "Make sure all players are on the same Wi-Fi network and have Bluetooth enabled for the best results." } else { return "Practice your skills in solo races. Solo races will not count towards your stats." } @@ -49,9 +49,11 @@ extension MPCHostViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.section == 1 { - return tableView.dequeueReusableCell(withIdentifier: "soloCell", for: indexPath) + return tableView.dequeueReusableCell(withIdentifier: MPCHostSoloCell.reuseIdentifier, + for: indexPath) } else if peers.isEmpty { - return tableView.dequeueReusableCell(withIdentifier: MPCHostSearchingCell.reuseIdentifier, for: indexPath) + return tableView.dequeueReusableCell(withIdentifier: MPCHostSearchingCell.reuseIdentifier, + for: indexPath) } //swiftlint:disable:next line_length @@ -76,13 +78,13 @@ extension MPCHostViewController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - PlayerMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .userAction(#function)) if indexPath.section == 1 { - PlayerMetrics.log(event: .hostStartedSoloMatch) + PlayerAnonymousMetrics.log(event: .hostStartedSoloMatch) session?.disconnect() - didStartMatch?(true) + listenerUpdate?(.startMatch(isSolo: true)) tableView.isUserInteractionEnabled = false return } @@ -90,6 +92,11 @@ extension MPCHostViewController { // Hits this case when the "Searching..." placeholder cell is selected guard !peers.isEmpty else { return } + let maxPlayerCount = min(WKRKitConstants.current.maxLocalRacePlayers, + kMCSessionMaximumNumberOfPeers) + let peerCount = session?.connectedPeers.count ?? 0 + guard maxPlayerCount > peerCount + 1 else { return } + let peerID = sortedPeers[indexPath.row] guard let session = session else { fatalError("Session is nil") @@ -104,8 +111,29 @@ extension MPCHostViewController { } update(peerID: peerID, to: .invited) - browser?.invitePeer(peerID, to: session, withContext: nil, timeout: 15.0) + + let appInfo = Bundle.main.appInfo + let context = MPCHostContext(appBuild: appInfo.build, + appVersion: appInfo.version, + name: session.myPeerID.displayName, + inviteTimeout: 45.0, + minPeerAppBuild: 3706) + guard let data = try? JSONEncoder().encode(context) else { + fatalError("Couldn't encode context") + } + + browser?.invitePeer(peerID, + to: session, + withContext: data, + timeout: context.inviteTimeout) tableView.deselectRow(at: indexPath, animated: true) } + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if (indexPath.section == 0 && peers.isEmpty) || indexPath.section == 1 { + return 44.0 + } + return super.tableView(tableView, heightForRowAt: indexPath) + } + } diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController.swift b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController.swift similarity index 74% rename from WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController.swift rename to WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController.swift index 5a1823c..2b033df 100644 --- a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCHostViewController/MPCHostViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostViewController/MPCHostViewController.swift @@ -9,11 +9,14 @@ import MultipeerConnectivity import UIKit +import WKRKit +import WKRUIKit + #if !MULTIWINDOWDEBUG import FirebasePerformance #endif -internal class MPCHostViewController: StateLogTableViewController, MCSessionDelegate, MCNearbyServiceBrowserDelegate { +internal class MPCHostViewController: UITableViewController, MCSessionDelegate, MCNearbyServiceBrowserDelegate { // MARK: - Types @@ -25,6 +28,10 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele case declined } + enum ListenerUpdate { + case startMatch(isSolo: Bool) + case cancel + } // MARK: - Properties var peers = [MCPeerID: PeerState]() @@ -43,15 +50,14 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele var serviceType: String? var browser: MCNearbyServiceBrowser? - /// Called when the start button is pressed - var didStartMatch: ((_ isSolo: Bool) -> Void)? - /// Called when the cancel button is pressed - var didCancelMatch: (() -> Void)? + var listenerUpdate: ((ListenerUpdate) -> Void)? // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() + title = "INVITE NEARBY PLAYERS" + guard let peerID = peerID, let serviceType = serviceType else { fatalError("Required properties peerID or serviceType not set") } @@ -59,13 +65,27 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele browser?.delegate = self session?.delegate = self - navigationItem.rightBarButtonItem?.isEnabled = false + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, + target: self, + action: #selector(cancelMatch(_:))) + + let startButton = UIBarButtonItem(barButtonSystemItem: .play, + target: self, + action: #selector(startMatch(_:))) + startButton.isEnabled = false + navigationItem.rightBarButtonItem = startButton navigationController?.navigationBar.barStyle = UIBarStyle.wkrStyle + tableView.backgroundColor = WKRUIStyle.isDark ? UIColor.wkrBackgroundColor : UIColor.groupTableViewBackground + tableView.estimatedRowHeight = 150 tableView.rowHeight = UITableView.automaticDimension tableView.register(MPCHostPeerStateCell.self, forCellReuseIdentifier: MPCHostPeerStateCell.reuseIdentifier) + tableView.register(MPCHostSearchingCell.self, + forCellReuseIdentifier: MPCHostSearchingCell.reuseIdentifier) + tableView.register(MPCHostSoloCell.self, + forCellReuseIdentifier: MPCHostSoloCell.reuseIdentifier) } override func viewWillAppear(_ animated: Bool) { @@ -82,16 +102,18 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele // MARK: - Actions - @IBAction func cancelMatch(_ sender: Any) { - PlayerMetrics.log(event: .userAction(#function)) - PlayerMetrics.log(event: .hostCancelledPreMatch) + @objc + func cancelMatch(_ sender: Any) { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .hostCancelledPreMatch) session?.disconnect() - didCancelMatch?() + listenerUpdate?(.cancel) } - @IBAction func startMatch(_ sender: Any) { - PlayerMetrics.log(event: .userAction(#function)) + @objc + func startMatch(_ sender: Any) { + PlayerAnonymousMetrics.log(event: .userAction(#function)) tableView.isUserInteractionEnabled = false @@ -105,17 +127,19 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele guard let session = session else { fatalError("Session is nil") } do { - // Participants move to the game view and wait for "real" data when they receive first data chunk - let data = Data(bytes: [1]) - let peers = session.connectedPeers - try session.send(data, toPeers: peers, with: .unreliable) - try session.send(data, toPeers: peers, with: .reliable) + let message = ConnectViewController.StartMessage(hostName: session.myPeerID.displayName) + let data = try JSONEncoder().encode(message) + try session.send(data, toPeers: session.connectedPeers, with: .reliable) + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - self.didStartMatch?(false) + self.listenerUpdate?(.startMatch(isSolo: false)) } } catch { + let info = "startMatch: " + error.localizedDescription + PlayerAnonymousMetrics.log(event: .error(info)) + session.disconnect() - didCancelMatch?() + listenerUpdate?(.cancel) } } @@ -126,10 +150,10 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele /// - newState: The new state func update(peerID: MCPeerID, to newState: PeerState?) { let newStateString = String(describing: newState?.rawValue) - PlayerMetrics.log(event: .gameState("Peer Update: \(peerID.displayName) \(newStateString)")) + PlayerAnonymousMetrics.log(event: .gameState("Peer Update: \(peerID.displayName) \(newStateString)")) guard let newState = newState else { - if let index = sortedPeers.index(of: peerID) { + if let index = sortedPeers.firstIndex(of: peerID) { peers[peerID] = nil if peers.isEmpty { tableView.reloadRows(at: [IndexPath(row: index)], with: .fade) @@ -142,14 +166,14 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele if let state = peers[peerID], state != newState { peers[peerID] = newState - if let index = sortedPeers.index(of: peerID) { + if let index = sortedPeers.firstIndex(of: peerID) { tableView.reloadRows(at: [IndexPath(row: index)], with: .fade) } else { tableView.reloadData() } } else if peers[peerID] == nil { peers[peerID] = newState - if let index = sortedPeers.index(of: peerID) { + if let index = sortedPeers.firstIndex(of: peerID) { if peers.count == 1 { tableView.reloadRows(at: [IndexPath(row: index)], with: .fade) } else { @@ -202,7 +226,7 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele } func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) { - didCancelMatch?() + listenerUpdate?(.cancel) } func browser(_ browser: MCNearbyServiceBrowser, @@ -226,15 +250,20 @@ internal class MPCHostViewController: StateLogTableViewController, MCSessionDele case .connected: self.update(peerID: peerID, to: .joined) UINotificationFeedbackGenerator().notificationOccurred(.success) + @unknown default: + return } } } - // MARK: - Unused MCSessionDelegate - func session(_ session: MCSession, didReceive data: Data, - fromPeer peerID: MCPeerID) {} + fromPeer peerID: MCPeerID) { + WKRSeenFinalArticlesStore.addRemoteTransferData(data) + } + + // MARK: - Unused MCSessionDelegate + func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, diff --git a/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewCell.swift b/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewCell.swift index 4d35c82..069f997 100644 --- a/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewCell.swift +++ b/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewCell.swift @@ -20,45 +20,8 @@ class DebugInfoTableViewCell: UITableViewCell { super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) guard let textLabel = textLabel, let detailTextLabel = detailTextLabel else { fatalError() } - - textLabel.removeConstraints(textLabel.constraints) - detailTextLabel.removeConstraints(detailTextLabel.constraints) - - textLabel.translatesAutoresizingMaskIntoConstraints = false - detailTextLabel.translatesAutoresizingMaskIntoConstraints = false - textLabel.numberOfLines = 0 detailTextLabel.numberOfLines = 0 - - let leftMarginConstraint = NSLayoutConstraint(item: textLabel, - attribute: .left, - relatedBy: .equal, - toItem: self, - attribute: .leftMargin, - multiplier: 1.0, - constant: 0.0) - - let rightMarginConstraint = NSLayoutConstraint(item: textLabel, - attribute: .right, - relatedBy: .equal, - toItem: self, - attribute: .rightMargin, - multiplier: 1.0, - constant: 0.0) - - let verticalConstant: CGFloat = 12.0 - let constraints = [ - leftMarginConstraint, - rightMarginConstraint, - textLabel.topAnchor.constraint(equalTo: topAnchor, constant: verticalConstant), - textLabel.bottomAnchor.constraint(equalTo: detailTextLabel.topAnchor), - - detailTextLabel.leftAnchor.constraint(equalTo: textLabel.leftAnchor), - detailTextLabel.rightAnchor.constraint(equalTo: textLabel.rightAnchor), - detailTextLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -verticalConstant) - ] - - NSLayoutConstraint.activate(constraints) } required init?(coder aDecoder: NSCoder) { diff --git a/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewController.swift b/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewController.swift index d021be9..42cbb29 100644 --- a/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/DebugInfoTableViewController/DebugInfoTableViewController.swift @@ -25,6 +25,9 @@ class DebugInfoTableViewController: UITableViewController { forCellReuseIdentifier: DebugInfoTableViewCell.reuseIdentifier) navigationController?.navigationBar.barStyle = UIBarStyle.wkrStyle + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, + target: self, + action: #selector(share(_:))) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(done)) @@ -37,6 +40,17 @@ class DebugInfoTableViewController: UITableViewController { dismiss(animated: true, completion: nil) } + @objc + func share(_ sender: UIBarButtonItem) { + var string = "WikiRaces Debug:\n" + for (key, value) in info { + string += "\n\nKey: \(key)\nValue:\n\(String(describing: value))" + } + let activityViewController = UIActivityViewController(activityItems: [string], applicationActivities: nil) + activityViewController.popoverPresentationController?.barButtonItem = sender + present(activityViewController, animated: true, completion: nil) + } + // MARK: - UITableViewDataSource override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -55,4 +69,31 @@ class DebugInfoTableViewController: UITableViewController { return cell } + + // UITableViewDelegate + + override func tableView(_ tableView: UITableView, + canPerformAction action: Selector, + forRowAt indexPath: IndexPath, + withSender sender: Any?) -> Bool { + return action == #selector(copy(_:)) + } + + override func tableView(_ tableView: UITableView, + performAction action: Selector, + forRowAt indexPath: IndexPath, + withSender sender: Any?) { + guard action == #selector(copy(_:)), + let cell = tableView.cellForRow(at: indexPath), + let text = cell.textLabel?.text, + let detail = cell.detailTextLabel?.text else { return } + + let string = "Key: \(text)\nValue:\n\(detail)" + UIPasteboard.general.string = string + } + + override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { + return true + } + } diff --git a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController.swift b/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController.swift deleted file mode 100644 index efb98ac..0000000 --- a/WikiRaces/Shared/Menu View Controllers/MPC Flow Controllers/MPCConnectViewController/MPCConnectViewController.swift +++ /dev/null @@ -1,278 +0,0 @@ -// -// MPCConnectViewController.swift -// WikiRaces -// -// Created by Andrew Finke on 9/6/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import GameKit -import MultipeerConnectivity -import UIKit - -import WKRKit -import WKRUIKit - -#if !MULTIWINDOWDEBUG -import FirebasePerformance -#endif - -internal class MPCConnectViewController: StateLogViewController { - - // MARK: - Interafce Elements - - /// General status label - @IBOutlet weak var descriptionLabel: UILabel! - /// Activity spinner - @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView! - /// The button to cancel joining/creating a race - @IBOutlet weak var cancelButton: UIButton! - - let inviteView = UIView() - let hostNameLabel = UILabel() - let acceptButton = UIButton() - let declineButton = UIButton() - - // MARK: - Properties - - var playerName = UIDevice.current.name - var isValidPlayerName = false - - var isSolo = false - var isPlayerHost = false - - var isFirstAppear = true - var isShowingMatch = false - var isShowingInvite = false - - // MARK: - MPC Properties - - var advertiser: MCNearbyServiceAdvertiser? - var activeInvite: ((Bool, MCSession) -> Void)? - var invites = [(handler: ((Bool, MCSession) -> Void)?, host: MCPeerID)]() - - var peerID: MCPeerID! - var hostPeerID: MCPeerID? - - let serviceType = "WKRPeer30" - lazy var session: MCSession = { - return MCSession(peer: self.peerID) - }() - - #if !MULTIWINDOWDEBUG - var connectingTrace: Trace? - #endif - - // MARK: - View Life Cycle - - override func viewDidLoad() { - super.viewDidLoad() - - // Gets either the player name specified in settings.app, then GK alias, the device name - if let name = UserDefaults.standard.object(forKey: "name_preference") as? String { - playerName = name - PlayerMetrics.log(event: .usingCustomName(playerName)) - } else if GKLocalPlayer.local.isAuthenticated { - playerName = GKLocalPlayer.local.alias - PlayerMetrics.log(event: .usingGCAlias(playerName)) - } else { - PlayerMetrics.log(event: .usingDeviceName(playerName)) - } - - #if !MULTIWINDOWDEBUG - Crashlytics.sharedInstance().setUserName(playerName) - #endif - - isValidPlayerName = [UInt8](playerName.utf8).count < 40 - guard isValidPlayerName else { return } - - // Uses existing peer ID object if already created (recommended per Apple docs) - if let pastPeerIDData = UserDefaults.standard.data(forKey: "PeerID"), - let lastPeerID = NSKeyedUnarchiver.unarchiveObject(with: pastPeerIDData) as? MCPeerID, - lastPeerID.displayName == playerName { - peerID = lastPeerID - } else { - peerID = MCPeerID(displayName: playerName) - let data = NSKeyedArchiver.archivedData(withRootObject: peerID) - UserDefaults.standard.set(data, forKey: "PeerID") - } - - setupInterface() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - advertiser?.stopAdvertisingPeer() - - // Reject all the pending invites - for invite in invites { - invite.handler?(false, session) - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - guard isFirstAppear else { - return - } - isFirstAppear = false - - // Test the connection to Wikipedia - - #if !MULTIWINDOWDEBUG - let trace = Performance.startTrace(name: "Connection Test Trace") - #endif - WKRConnectionTester.start { (success) in - DispatchQueue.main.async { - if success && self.isValidPlayerName { - if self.isPlayerHost { - UIView.animate(withDuration: 0.25, animations: { - self.descriptionLabel.alpha = 0.0 - self.inviteView.alpha = 0.0 - self.cancelButton.alpha = 0.0 - self.activityIndicatorView.alpha = 0.0 - }, completion: { _ in - self.performSegue(withIdentifier: "showHost", sender: nil) - }) - } else { - self.startAdvertising() - } - #if !MULTIWINDOWDEBUG - trace?.stop() - #endif - } else if !success { - self.showError(title: "Slow Connection", - message: "A fast internet connection is required to play WikiRaces.") - } - } - } - - UIView.animate(withDuration: 0.5, animations: { - self.descriptionLabel.alpha = 1.0 - self.activityIndicatorView.alpha = 1.0 - self.cancelButton.alpha = 1.0 - }) - - if !isValidPlayerName { - showError(title: "Player Name Too Long", - message: "Your player name is too long. Would you like to open settings to adjust it?", - showSettingsButton: true) - } - } - - // MARK: - Interface Updates - - func updateDescriptionLabel(to text: String) { - descriptionLabel.attributedText = NSAttributedString(string: text, - spacing: 2.0, - font: UIFont.systemFont(ofSize: 18.0, weight: .semibold)) - } - - /// Shows an error with a title - /// - /// - Parameters: - /// - title: The title of the error message - /// - message: The message body of the error - func showError(title: String, message: String, showSettingsButton: Bool = false) { - - session.delegate = nil - session.disconnect() - - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let action = UIAlertAction(title: "Menu", style: .default) { _ in - self.pressedCancelButton() - } - alertController.addAction(action) - - if showSettingsButton { - let settingsAction = UIAlertAction(title: "Open Settings", style: .default, handler: { _ in - PlayerMetrics.log(event: .userAction("showError:settings")) - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { - fatalError("Settings URL nil") - } - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) - self.pressedCancelButton() - }) - alertController.addAction(settingsAction) - } - - present(alertController, animated: true, completion: nil) - PlayerMetrics.log(presentingOf: alertController, on: self) - } - - /// Prepares to start the match - /// - /// - Parameter isPlayerHost: Is the local player is host - func showMatch(isPlayerHost: Bool) { - guard !isShowingMatch else { return } - isShowingMatch = true - - DispatchQueue.main.async { - UIView.animate(withDuration: 0.25, animations: { - self.descriptionLabel.alpha = 0.0 - self.inviteView.alpha = 0.0 - self.cancelButton.alpha = 0.0 - self.activityIndicatorView.alpha = 0.0 - }, completion: { _ in - self.performSegue(withIdentifier: "showRace", sender: isPlayerHost) - }) - - if !isPlayerHost { - UINotificationFeedbackGenerator().notificationOccurred(.success) - } - } - } - - /// Cancels the join/create a race action and sends player back to main menu - @IBAction func pressedCancelButton() { - PlayerMetrics.log(event: .userAction(#function)) - UIView.animate(withDuration: 0.25, animations: { - self.descriptionLabel.alpha = 0.0 - self.inviteView.alpha = 0.0 - self.activityIndicatorView.alpha = 0.0 - self.cancelButton.alpha = 0.0 - }, completion: { _ in - self.navigationController?.popToRootViewController(animated: false) - }) - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - guard let navigationController = segue.destination as? UINavigationController else { - fatalError("Destination is not a UINavigationController") - } - - if segue.identifier == "showHost" { - guard let destination = navigationController.rootViewController as? MPCHostViewController else { - fatalError("Destination rootViewController is not a MPCHostViewController") - } - destination.peerID = peerID - destination.session = session - destination.serviceType = serviceType - destination.didStartMatch = { [weak self] isSolo in - self?.isSolo = isSolo - self?.dismiss(animated: true, completion: { - self?.showMatch(isPlayerHost: true) - }) - } - destination.didCancelMatch = { [weak self] in - self?.dismiss(animated: true, completion: { - self?.navigationController?.popToRootViewController(animated: false) - }) - } - } else { - guard let destination = navigationController.rootViewController as? GameViewController, - let isPlayerHost = sender as? Bool else { - fatalError("Destination rootViewController is not a GameViewController") - } - if isSolo { - destination.networkConfig = .solo(name: playerName) - } else { - destination.networkConfig = .mpc(mpcServiceType: serviceType, - session: session, - isHost: isPlayerHost) - } - } - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MedalScene.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MedalScene.swift new file mode 100644 index 0000000..9065e14 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MedalScene.swift @@ -0,0 +1,129 @@ +// +// MedalScene.swift +// WikiRaces +// +// Created by Andrew Finke on 3/6/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import SpriteKit + +class MedalScene: SKScene { + + // MARK: Properties + + private let goldNode: SKNode + private let silverNode: SKNode + private let bronzeNode: SKNode + private let dnfNode: SKNode + + public var isActive = false { + didSet { + isPaused = !isActive + } + } + + // MARK: - Initalization + + override init(size: CGSize) { + let physicsBody = SKPhysicsBody(circleOfRadius: 5) + physicsBody.allowsRotation = true + physicsBody.linearDamping = 1.75 + physicsBody.angularDamping = 0.75 + + func texture(for text: String, width: CGFloat = 50) -> SKTexture { + let size = CGSize(width: width, height: width) + let emojiRect = CGRect(origin: .zero, size: size) + let emojiRenderer = UIGraphicsImageRenderer(size: size) + return SKTexture(image: emojiRenderer.image { context in + UIColor.clear.setFill() + context.fill(emojiRect) + let text = text + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: width - 5) + ] + text.draw(in: emojiRect, withAttributes: attributes) + }) + } + + let goldNode = SKSpriteNode(texture: texture(for: "🥇")) + goldNode.physicsBody = physicsBody + self.goldNode = goldNode + let silverNode = SKSpriteNode(texture: texture(for: "🥈")) + silverNode.physicsBody = physicsBody + self.silverNode = silverNode + let bronzeNode = SKSpriteNode(texture: texture(for: "🥉")) + bronzeNode.physicsBody = physicsBody + self.bronzeNode = bronzeNode + let dnfNode = SKSpriteNode(texture: texture(for: "❌", width: 20)) + dnfNode.physicsBody = physicsBody + self.dnfNode = dnfNode + + super.init(size: size) + anchorPoint = CGPoint(x: 0, y: 0) + backgroundColor = .clear + physicsWorld.gravity = CGVector(dx: 0, dy: -7) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Update + + override func update(_ currentTime: TimeInterval) { + for node in children where node.position.y < -50 { + node.removeFromParent() + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.isActive = !self.children.isEmpty + } + } + + // MARK: - Other + + func showMedals(gold: Int, silver: Int, bronze: Int, dnf: Int) { + func createMedal(place: Int) { + let node: SKNode + if place == 1, let copy = goldNode.copy() as? SKNode { + node = copy + } else if place == 2, let copy = silverNode.copy() as? SKNode { + node = copy + } else if place == 3, let copy = bronzeNode.copy() as? SKNode { + node = copy + } else if place == 4, let copy = dnfNode.copy() as? SKNode { + node = copy + } else { + return + } + + let padding: CGFloat = 40 + let maxX = size.width - padding + node.position = CGPoint(x: CGFloat.random(in: padding.. 0 else { return } + medalScene.showMedals(gold: Int(firstMedals), + silver: Int(secondMedals), + bronze: Int(thirdMedals), + dnf: Int(dnfCount)) + + medalScene.isActive = true + UIImpactFeedbackGenerator().impactOccurred() + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuTile.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuTile.swift similarity index 71% rename from WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuTile.swift rename to WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuTile.swift index 8245300..e06ac52 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuTile.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuTile.swift @@ -17,13 +17,29 @@ internal class MenuTile: UIControl { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 + formatter.groupingSeparator = "" // 1,000 -> 1000 + return formatter + }() + + static private let fractionalNumberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 2 + return formatter + }() + + static private let fractionalLargeNumberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 1 return formatter }() private let titleLabel = UILabel() private let valueLabel = UILabel() - var stat: StatsHelper.Stat? + var isAverage = false var title: String? { set { guard let text = newValue else { @@ -42,12 +58,18 @@ internal class MenuTile: UIControl { var value: Double? { set { - guard let value = newValue, - let formattedValue = MenuTile.numberFormatter.string(from: NSNumber(value: value)) else { + guard let value = newValue else { valueLabel.text = nil return } - + var formattedValue = MenuTile.numberFormatter.string(from: NSNumber(value: value)) + if isAverage { + formattedValue = MenuTile.fractionalNumberFormatter.string(from: NSNumber(value: value)) + } else if value >= 1000 { + let adjusted = NSNumber(value: value / 1000) + let formatterValue = (MenuTile.fractionalLargeNumberFormatter.string(from: adjusted) ?? "0") + formattedValue = formatterValue + "K" + } valueLabel.text = formattedValue } get { @@ -59,10 +81,10 @@ internal class MenuTile: UIControl { override var bounds: CGRect { didSet { if bounds.width > 200 { - titleLabel.font = UIFont.boldSystemFont(ofSize: 18) + titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .semibold) valueLabel.font = UIFont.systemFont(ofSize: 46) } else if bounds.width > 100 { - titleLabel.font = UIFont.boldSystemFont(ofSize: 16) + titleLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold) valueLabel.font = UIFont.systemFont(ofSize: 38) } else { titleLabel.font = UIFont.systemFont(ofSize: 11, weight: .bold) diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Actions.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Actions.swift new file mode 100644 index 0000000..48cecaf --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Actions.swift @@ -0,0 +1,168 @@ +// +// MenuView+Actions.swift +// WikiRaces +// +// Created by Andrew Finke on 2/23/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import GameKit.GKLocalPlayer + +extension MenuView { + + // MARK: - Actions + + /// Join button pressed + @objc + func showLocalRaceOptions() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .pressedLocalOptions) + + UISelectionFeedbackGenerator().selectionChanged() + + animateOptionsOutAndTransition(to: .localOptions) + } + + @objc + func joinGlobalRace() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .pressedGlobalJoin) + PlayerDatabaseStat.gkPressedJoin.increment() + + UISelectionFeedbackGenerator().selectionChanged() + + guard GKLocalPlayer.local.isAuthenticated else { + self.listenerUpdate?(.presentGlobalAuth) + return + } + + animateMenuOut { + self.listenerUpdate?(.presentGlobalConnect) + } + } + + /// Join button pressed + @objc + func joinLocalRace() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .pressedJoin) + PlayerDatabaseStat.mpcPressedJoin.increment() + + UISelectionFeedbackGenerator().selectionChanged() + + guard !promptForCustomName(isHost: false) else { + return + } + + animateMenuOut { + self.listenerUpdate?(.presentMPCConnect(isHost: false)) + } + } + + /// Create button pressed + @objc + func createLocalRace() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .pressedHost) + PlayerDatabaseStat.mpcPressedHost.increment() + + UISelectionFeedbackGenerator().selectionChanged() + + guard !promptForCustomName(isHost: true) else { + return + } + + animateMenuOut { + self.listenerUpdate?(.presentMPCConnect(isHost: true)) + } + } + + @objc + func localOptionsBackButtonPressed() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + + UISelectionFeedbackGenerator().selectionChanged() + + animateOptionsOutAndTransition(to: .raceTypeOptions) + } + + /// Called when a tile is pressed + /// + /// - Parameter sender: The pressed tile + @objc + func menuTilePressed(sender: MenuTile) { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + + guard GKLocalPlayer.local.isAuthenticated else { + self.listenerUpdate?(.presentGlobalAuth) + return + } + + animateMenuOut { + self.listenerUpdate?(.presentLeaderboard) + } + } + + func triggeredEasterEgg() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + medalView.showMedals() + } + + // MARK: - Menu Animations + + /// Animates the views on screen + func animateMenuIn(completion: (() -> Void)? = nil) { + isUserInteractionEnabled = false + + movingPuzzleView.start() + + state = .raceTypeOptions + setNeedsLayout() + + UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle, + animations: { + self.layoutIfNeeded() + }, completion: { _ in + self.isUserInteractionEnabled = true + completion?() + }) + } + + /// Animates the views off screen + /// + /// - Parameter completion: The completion handler + func animateMenuOut(completion: (() -> Void)?) { + isUserInteractionEnabled = false + + state = .noInterface + setNeedsLayout() + + UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle, animations: { + self.layoutIfNeeded() + }, completion: { _ in + self.movingPuzzleView.stop() + completion?() + }) + } + + func animateOptionsOutAndTransition(to state: InterfaceState) { + self.state = .noOptions + setNeedsLayout() + + UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle / 2, + animations: { + self.layoutIfNeeded() + }, completion: { _ in + self.state = state + self.setNeedsLayout() + + UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle / 2, + delay: WKRAnimationDurationConstants.menuToggle / 4, + animations: { + self.layoutIfNeeded() + }) + }) + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Setup.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Setup.swift new file mode 100644 index 0000000..3b2b7b1 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView+Setup.swift @@ -0,0 +1,230 @@ +// +// MenuView+Setup.swift +// WikiRaces +// +// Created by Andrew Finke on 2/23/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import WKRUIKit + +extension MenuView { + + // MARK: - Top View + + /// Sets up the top view of the menu + //swiftlint:disable:next function_body_length + func setupTopView() { + setupLabels() + setupButtons() + medalView.translatesAutoresizingMaskIntoConstraints = false + topView.addSubview(medalView) + + titleLabelConstraint = titleLabel.topAnchor.constraint(equalTo: topView.safeAreaLayoutGuide.topAnchor) + + localRaceTypeButtonWidthConstraint = localRaceTypeButton.widthAnchor.constraint(equalToConstant: 0) + localRaceTypeButtonHeightConstraint = localRaceTypeButton.heightAnchor.constraint(equalToConstant: 0) + localRaceTypeButtonLeftConstraint = localRaceTypeButton.leftAnchor.constraint(equalTo: topView.leftAnchor, + constant: 0) + globalRaceTypeButtonWidthConstraint = globalRaceTypeButton.widthAnchor.constraint(equalToConstant: 0) + + joinLocalRaceButtonWidthConstraint = joinLocalRaceButton.widthAnchor.constraint(equalToConstant: 0) + joinLocalRaceButtonLeftConstraint = joinLocalRaceButton.leftAnchor.constraint(equalTo: topView.leftAnchor, + constant: 0) + createLocalRaceButtonWidthConstraint = createLocalRaceButton.widthAnchor.constraint(equalToConstant: 0) + localOptionsBackButtonWidth = localOptionsBackButton.widthAnchor.constraint(equalToConstant: 30) + + let constraints = [ + titleLabelConstraint!, + + medalView.topAnchor.constraint(equalTo: topView.topAnchor), + medalView.bottomAnchor.constraint(equalTo: topView.bottomAnchor), + medalView.leftAnchor.constraint(equalTo: topView.leftAnchor), + medalView.rightAnchor.constraint(equalTo: topView.rightAnchor), + + titleLabel.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 30), + titleLabel.widthAnchor.constraint(equalTo: topView.widthAnchor), + + subtitleLabel.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 30), + subtitleLabel.widthAnchor.constraint(equalTo: topView.widthAnchor), + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), + + localRaceTypeButtonWidthConstraint!, + localRaceTypeButtonHeightConstraint!, + localRaceTypeButtonLeftConstraint!, + globalRaceTypeButtonWidthConstraint!, + + joinLocalRaceButtonWidthConstraint!, + joinLocalRaceButtonLeftConstraint!, + createLocalRaceButtonWidthConstraint!, + localOptionsBackButtonWidth!, + + localRaceTypeButton.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, + constant: 40.0), + joinLocalRaceButton.topAnchor.constraint(equalTo: localRaceTypeButton.topAnchor), + + globalRaceTypeButton.heightAnchor.constraint(equalTo: localRaceTypeButton.heightAnchor), + joinLocalRaceButton.heightAnchor.constraint(equalTo: localRaceTypeButton.heightAnchor), + createLocalRaceButton.heightAnchor.constraint(equalTo: localRaceTypeButton.heightAnchor), + + globalRaceTypeButton.leftAnchor.constraint(equalTo: localRaceTypeButton.leftAnchor), + createLocalRaceButton.leftAnchor.constraint(equalTo: joinLocalRaceButton.leftAnchor), + + globalRaceTypeButton.topAnchor.constraint(equalTo: localRaceTypeButton.bottomAnchor, + constant: 20.0), + createLocalRaceButton.topAnchor.constraint(equalTo: globalRaceTypeButton.topAnchor), + + localOptionsBackButton.leftAnchor.constraint(equalTo: joinLocalRaceButton.leftAnchor), + localOptionsBackButton.topAnchor.constraint(equalTo: createLocalRaceButton.bottomAnchor, + constant: 20.0), + + localOptionsBackButton.heightAnchor.constraint(equalTo: localOptionsBackButton.widthAnchor, + multiplier: 1) + ] + NSLayoutConstraint.activate(constraints) + } + + /// Sets up the buttons + private func setupButtons() { + localRaceTypeButton.title = "local race" + localRaceTypeButton.translatesAutoresizingMaskIntoConstraints = false + localRaceTypeButton.addTarget(self, action: #selector(showLocalRaceOptions), for: .touchUpInside) + topView.addSubview(localRaceTypeButton) + + globalRaceTypeButton.title = "global race" + globalRaceTypeButton.translatesAutoresizingMaskIntoConstraints = false + globalRaceTypeButton.addTarget(self, action: #selector(joinGlobalRace), for: .touchUpInside) + topView.addSubview(globalRaceTypeButton) + + joinLocalRaceButton.title = "join race" + joinLocalRaceButton.translatesAutoresizingMaskIntoConstraints = false + joinLocalRaceButton.addTarget(self, action: #selector(joinLocalRace), for: .touchUpInside) + topView.addSubview(joinLocalRaceButton) + + createLocalRaceButton.title = "create race" + createLocalRaceButton.translatesAutoresizingMaskIntoConstraints = false + createLocalRaceButton.addTarget(self, action: #selector(createLocalRace), for: .touchUpInside) + topView.addSubview(createLocalRaceButton) + + localOptionsBackButton.setImage(UIImage(named: "Back")!, for: .normal) + localOptionsBackButton.tintColor = .wkrTextColor + localOptionsBackButton.translatesAutoresizingMaskIntoConstraints = false + localOptionsBackButton.addTarget(self, action: #selector(localOptionsBackButtonPressed), for: .touchUpInside) + topView.addSubview(localOptionsBackButton) + + localOptionsBackButton.layer.borderWidth = 1.7 + localOptionsBackButton.layer.borderColor = UIColor.wkrTextColor.cgColor + } + + /// Sets up the labels + private func setupLabels() { + titleLabel.text = "WikiRaces" + titleLabel.textColor = UIColor.wkrTextColor + titleLabel.translatesAutoresizingMaskIntoConstraints = false + topView.addSubview(titleLabel) + + #if DEBUG + if !UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + titleLabel.textColor = UIColor(red: 51.0 / 255.0, + green: 102.0 / 255.0, + blue: 204.0 / 255.0, + alpha: 1.0) + } + #endif + + subtitleLabel.text = "Conquer the encyclopedia\nof everything." + subtitleLabel.numberOfLines = 2 + subtitleLabel.textColor = UIColor.wkrTextColor + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + subtitleLabel.clipsToBounds = true + topView.addSubview(subtitleLabel) + } + + // MARK: - Bottom View + + /// Sets up the bottom views + func setupBottomView() { + let stackView = setupStatsStackView() + bottomView.addSubview(movingPuzzleView) + puzzleViewHeightConstraint = movingPuzzleView.heightAnchor.constraint(equalToConstant: 75) + + let constraints = [ + puzzleViewHeightConstraint!, + movingPuzzleView.leftAnchor.constraint(equalTo: bottomView.leftAnchor), + movingPuzzleView.rightAnchor.constraint(equalTo: bottomView.rightAnchor), + movingPuzzleView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor), + + stackView.leftAnchor.constraint(equalTo: bottomView.leftAnchor, constant: 15), + stackView.rightAnchor.constraint(equalTo: bottomView.rightAnchor, constant: -15), + stackView.topAnchor.constraint(equalTo: bottomView.topAnchor), + stackView.bottomAnchor.constraint(equalTo: movingPuzzleView.topAnchor) + ] + NSLayoutConstraint.activate(constraints) + } + + /// Sets up the stack view that holds the menu tiles + //swiftlint:disable:next function_body_length + private func setupStatsStackView() -> UIStackView { + let statsStackView = UIStackView() + statsStackView.distribution = .fillEqually + statsStackView.translatesAutoresizingMaskIntoConstraints = false + bottomView.addSubview(statsStackView) + + let leftMenuTile = MenuTile(title: "WIKI POINTS") + leftMenuTile.value = PlayerStatsManager.shared.multiplayerPoints + statsStackView.addArrangedSubview(leftMenuTile) + + let middleMenuTile = MenuTile(title: "AVG PER RACE") + middleMenuTile.isAverage = true + middleMenuTile.value = PlayerDatabaseStat.multiplayerAverage.value() + statsStackView.addArrangedSubview(middleMenuTile) + + let leftThinLine = WKRUIThinLineView() + middleMenuTile.addSubview(leftThinLine) + let rightThinLine = WKRUIThinLineView() + middleMenuTile.addSubview(rightThinLine) + + let rightMenuTile = MenuTile(title: "RACES PLAYED") + rightMenuTile.value = PlayerStatsManager.shared.multiplayerRaces + statsStackView.addArrangedSubview(rightMenuTile) + + let constraints = [ + leftThinLine.leftAnchor.constraint(equalTo: middleMenuTile.leftAnchor), + leftThinLine.topAnchor.constraint(equalTo: middleMenuTile.topAnchor, constant: 30), + leftThinLine.bottomAnchor.constraint(equalTo: middleMenuTile.bottomAnchor, constant: -25), + leftThinLine.widthAnchor.constraint(equalToConstant: 2), + + rightThinLine.rightAnchor.constraint(equalTo: middleMenuTile.rightAnchor), + rightThinLine.topAnchor.constraint(equalTo: middleMenuTile.topAnchor, constant: 30), + rightThinLine.bottomAnchor.constraint(equalTo: middleMenuTile.bottomAnchor, constant: -25), + rightThinLine.widthAnchor.constraint(equalToConstant: 2) + ] + NSLayoutConstraint.activate(constraints) + + leftMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) + middleMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) + rightMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) + + self.leftMenuTile = leftMenuTile + self.middleMenuTile = middleMenuTile + self.rightMenuTile = rightMenuTile + + PlayerStatsManager.shared.menuStatsUpdated = { points, races, average in + DispatchQueue.main.async { + self.leftMenuTile?.value = points + self.middleMenuTile?.value = average + self.rightMenuTile?.value = races + } + } + + if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + self.leftMenuTile?.value = 140 + self.middleMenuTile?.value = 140/72 + self.rightMenuTile?.value = 72 + } + + return statsStackView + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView.swift new file mode 100644 index 0000000..8601a7f --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MenuView.swift @@ -0,0 +1,262 @@ +// +// MenuView.swift +// WikiRaces +// +// Created by Andrew Finke on 2/23/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import StoreKit +import WKRUIKit + +class MenuView: UIView { + + // MARK: Types + + enum InterfaceState { + case raceTypeOptions, noOptions, localOptions, noInterface + } + + enum ListenerUpdate { + case presentDebug, presentGlobalConnect, presentLeaderboard, presentGlobalAuth + case presentMPCConnect(isHost: Bool) + case presentAlert(UIAlertController) + } + + // MARK: - Closures + + var listenerUpdate: ((ListenerUpdate) -> Void)? + + // MARK: - Properties + + /// Used to track if the menu should be animating + var state = InterfaceState.noInterface + + // MARK: - Interface Elements + + /// The top of the menu (everything on white). Animates out of the left side. + let topView = UIView() + /// The bottom of the menu (everything not white). Animates out of the bottom. + let bottomView = UIView() + + /// The "WikiRaces" label + let titleLabel = UILabel() + /// The "Conquer..." label + let subtitleLabel = UILabel() + + let localRaceTypeButton = WKRUIButton() + let globalRaceTypeButton = WKRUIButton() + let joinLocalRaceButton = WKRUIButton() + let createLocalRaceButton = WKRUIButton() + let localOptionsBackButton = UIButton() + + /// The Wiki Points tile + var leftMenuTile: MenuTile? + /// The average points tile + var middleMenuTile: MenuTile? + /// The races tile + var rightMenuTile: MenuTile? + + /// The puzzle piece view + let movingPuzzleView = MovingPuzzleView() + + /// The easter egg medal view + let medalView = MedalView() + + // MARK: - Constraints + + /// Used to animate the top view in and out + var topViewLeftConstraint: NSLayoutConstraint! + /// Used to animate the bottom view in and out + var bottomViewAnchorConstraint: NSLayoutConstraint! + + /// Used for safe area layout adjustments + var bottomViewHeightConstraint: NSLayoutConstraint! + var puzzleViewHeightConstraint: NSLayoutConstraint! + + /// Used for adjusting y coord of title label based on screen height + var titleLabelConstraint: NSLayoutConstraint! + + /// Used for adjusting button widths and heights based on screen width + + var localRaceTypeButtonLeftConstraint: NSLayoutConstraint! + var localRaceTypeButtonWidthConstraint: NSLayoutConstraint! + var localRaceTypeButtonHeightConstraint: NSLayoutConstraint! + var globalRaceTypeButtonWidthConstraint: NSLayoutConstraint! + + var joinLocalRaceButtonLeftConstraint: NSLayoutConstraint! + var joinLocalRaceButtonWidthConstraint: NSLayoutConstraint! + var createLocalRaceButtonWidthConstraint: NSLayoutConstraint! + var localOptionsBackButtonWidth: NSLayoutConstraint! + + // MARK: - View Life Cycle + + init() { + super.init(frame: .zero) + + backgroundColor = UIColor.wkrBackgroundColor + UIApplication.shared.keyWindow?.backgroundColor = UIColor.wkrBackgroundColor + + let recognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognizerFired)) + recognizer.numberOfTapsRequired = 2 + recognizer.numberOfTouchesRequired = 2 + titleLabel.addGestureRecognizer(recognizer) + titleLabel.isUserInteractionEnabled = true + + topView.translatesAutoresizingMaskIntoConstraints = false + topView.backgroundColor = UIColor.wkrMenuTopViewColor + addSubview(topView) + + bottomView.backgroundColor = UIColor.wkrMenuBottomViewColor + bottomView.translatesAutoresizingMaskIntoConstraints = false + addSubview(bottomView) + + topViewLeftConstraint = topView.leftAnchor.constraint(equalTo: leftAnchor) + bottomViewAnchorConstraint = bottomView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 250) + bottomViewHeightConstraint = bottomView.heightAnchor.constraint(equalToConstant: 250) + + setupTopView() + setupBottomView() + + let constraints = [ + topView.topAnchor.constraint(equalTo: topAnchor), + topView.bottomAnchor.constraint(equalTo: bottomView.topAnchor), + topView.widthAnchor.constraint(equalTo: widthAnchor), + + bottomView.leftAnchor.constraint(equalTo: leftAnchor), + bottomView.widthAnchor.constraint(equalTo: widthAnchor), + bottomViewHeightConstraint!, + + topViewLeftConstraint!, + bottomViewAnchorConstraint! + ] + NSLayoutConstraint.activate(constraints) + } + + @objc func tapGestureRecognizerFired() { + listenerUpdate?(.presentDebug) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func safeAreaInsetsDidChange() { + super.safeAreaInsetsDidChange() + puzzleViewHeightConstraint.constant = 75 + safeAreaInsets.bottom / 2 + bottomViewHeightConstraint.constant = 250 + safeAreaInsets.bottom / 2 + } + + //swiftlint:disable:next function_body_length + override func layoutSubviews() { + super.layoutSubviews() + + // Button Styles + + let buttonStyle: WKRUIButtonStyle + let buttonWidth: CGFloat + let buttonHeight: CGFloat + if frame.size.width > 420 { + buttonStyle = .large + buttonWidth = 210 + buttonHeight = 50 + } else { + buttonStyle = .normal + buttonWidth = 175 + buttonHeight = 40 + } + + if frame.size.width < UIScreen.main.bounds.width / 1.8 { + leftMenuTile?.title = "WIKI\nPOINTS" + middleMenuTile?.title = "AVG PER\nRACE" + rightMenuTile?.title = "RACES\nPLAYED" + } else { + leftMenuTile?.title = "WIKI POINTS" + middleMenuTile?.title = "AVG PER RACE" + rightMenuTile?.title = "RACES PLAYED" + } + + localRaceTypeButton.style = buttonStyle + globalRaceTypeButton.style = buttonStyle + joinLocalRaceButton.style = buttonStyle + createLocalRaceButton.style = buttonStyle + + // Label Fonts + titleLabel.font = UIFont.systemFont(ofSize: min(frame.size.width / 10.0, 55), weight: .semibold) + subtitleLabel.font = UIFont.systemFont(ofSize: min(frame.size.width / 18.0, 30), weight: .medium) + + // Constraints + if UIDevice.current.userInterfaceIdiom == .pad { + titleLabelConstraint.constant = frame.size.height / 8 + } else { + titleLabelConstraint.constant = frame.size.height / 11 + } + + switch state { + case .raceTypeOptions: + localRaceTypeButtonLeftConstraint.constant = 30 + joinLocalRaceButtonLeftConstraint.constant = -createLocalRaceButton.frame.width + + topViewLeftConstraint.constant = 0 + bottomViewAnchorConstraint.constant = 0 + + if frame.size.height < 650 { + bottomViewAnchorConstraint.constant = 75 + } + case .noOptions: + localRaceTypeButtonLeftConstraint.constant = -globalRaceTypeButton.frame.width + joinLocalRaceButtonLeftConstraint.constant = -createLocalRaceButton.frame.width + case .localOptions: + localRaceTypeButtonLeftConstraint.constant = -globalRaceTypeButton.frame.width + joinLocalRaceButtonLeftConstraint.constant = 30 + case .noInterface: + topViewLeftConstraint.constant = -topView.frame.width + bottomViewAnchorConstraint.constant = bottomView.frame.height + localRaceTypeButtonLeftConstraint.constant = 30 + joinLocalRaceButtonLeftConstraint.constant = 30 + } + + localRaceTypeButtonHeightConstraint.constant = buttonHeight + localRaceTypeButtonWidthConstraint.constant = buttonWidth + 18 + globalRaceTypeButtonWidthConstraint.constant = buttonWidth + 32 + + joinLocalRaceButtonWidthConstraint.constant = buttonWidth + createLocalRaceButtonWidthConstraint.constant = buttonWidth + 30 + localOptionsBackButtonWidth.constant = buttonHeight - 10 + + localOptionsBackButton.layer.cornerRadius = localOptionsBackButtonWidth.constant / 2 + } + + func promptForCustomName(isHost: Bool) -> Bool { + guard !UserDefaults.standard.bool(forKey: "PromptedCustomName") else { + return false + } + UserDefaults.standard.set(true, forKey: "PromptedCustomName") + + let message = "Would you like to set a custom player name for local races?" + let alertController = UIAlertController(title: "Set Name?", message: message, preferredStyle: .alert) + + let laterAction = UIAlertAction(title: "Maybe Later", style: .cancel, handler: { _ in + PlayerAnonymousMetrics.log(event: .userAction("promptForCustomNamePrompt:rejected")) + PlayerAnonymousMetrics.log(event: .namePromptResult, attributes: ["Result": "Cancelled"]) + if isHost { + self.createLocalRace() + } else { + self.joinLocalRace() + } + }) + alertController.addAction(laterAction) + + let settingsAction = UIAlertAction(title: "Open Settings", style: .default, handler: { _ in + PlayerAnonymousMetrics.log(event: .userAction("promptForCustomNamePrompt:accepted")) + PlayerAnonymousMetrics.log(event: .namePromptResult, attributes: ["Result": "Accepted"]) + UIApplication.shared.openSettings() + }) + alertController.addAction(settingsAction) + + listenerUpdate?(.presentAlert(alertController)) + return true + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MovingPuzzleView.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MovingPuzzleView.swift new file mode 100644 index 0000000..8bbe332 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuView/MovingPuzzleView.swift @@ -0,0 +1,113 @@ +// +// MovingPuzzleView.swift +// WikiRaces +// +// Created by Andrew Finke on 2/25/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit + +class MovingPuzzleView: UIView, UIScrollViewDelegate { + + private var puzzleTimer: Timer? + /// The puzzle piece view + private let innerPuzzleView = UIScrollView() + + init() { + super.init(frame: .zero) + + backgroundColor = UIColor.wkrMenuPuzzleViewColor + translatesAutoresizingMaskIntoConstraints = false + + innerPuzzleView.delegate = self + innerPuzzleView.decelerationRate = .fast + innerPuzzleView.showsHorizontalScrollIndicator = false + innerPuzzleView.contentSize = CGSize(width: 20000, height: 30) + innerPuzzleView.backgroundColor = UIColor(patternImage: #imageLiteral(resourceName: "MenuBackgroundPuzzle")) + innerPuzzleView.translatesAutoresizingMaskIntoConstraints = false + addSubview(innerPuzzleView) + + let constraints = [ + innerPuzzleView.leftAnchor.constraint(equalTo: leftAnchor), + innerPuzzleView.rightAnchor.constraint(equalTo: rightAnchor), + innerPuzzleView.topAnchor.constraint(equalTo: topAnchor, constant: 22.5), + innerPuzzleView.heightAnchor.constraint(equalToConstant: 30) + ] + NSLayoutConstraint.activate(constraints) + + NotificationCenter.default.addObserver(self, + selector: #selector(stop), + name: UIApplication.didEnterBackgroundNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(start), + name: UIApplication.willEnterForegroundNotification, + object: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + let contentOffset = innerPuzzleView.contentOffset.x + if contentOffset > innerPuzzleView.contentSize.width * 0.8 { + animateContentOffsetReset() + } + PlayerAnonymousMetrics.log(event: .puzzleViewScrolled) + } + + private func animateContentOffsetReset() { + UIView.animate(withDuration: 0.25, + animations: { + self.innerPuzzleView.alpha = 0.0 + }, completion: { _ in + self.stop() + self.start() + UIView.animate(withDuration: 0.25, + animations: { + self.innerPuzzleView.alpha = 1.0 + }) + }) + } + + @objc + func start() { + innerPuzzleView.contentOffset = .zero + + let duration = TimeInterval(60) + let offset = CGFloat(40 * duration) + + func animateScroll() { + let contentOffset = innerPuzzleView.contentOffset.x + if contentOffset > innerPuzzleView.contentSize.width * 0.8 { + animateContentOffsetReset() + return + } + let xOffset = innerPuzzleView.contentOffset.x + offset + let options: UIView.AnimationOptions = [ + .curveLinear, + .allowUserInteraction + ] + UIView.animate(withDuration: duration, + delay: 0, + options: options, + animations: { + self.innerPuzzleView.contentOffset = CGPoint(x: xOffset, y: 0) + }, completion: nil) + } + + puzzleTimer?.invalidate() + puzzleTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: true) { _ in + animateScroll() + } + puzzleTimer?.fire() + } + + @objc + func stop() { + puzzleTimer?.invalidate() + innerPuzzleView.layer.removeAllAnimations() + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Debug.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Debug.swift new file mode 100644 index 0000000..7b8c325 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Debug.swift @@ -0,0 +1,91 @@ +// +// MenuViewController+Debug.swift +// WikiRaces +// +// Created by Andrew Finke on 1/27/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit + +import WKRKit +import WKRUIKit + +extension MenuViewController { + + @objc + func presentDebugController() { + PlayerAnonymousMetrics.log(event: .versionInfo) + + let message = "If your name isn't Andrew, you probably shouldn’t be here." + let alertController = UIAlertController(title: "Debug Panel", + message: message, + preferredStyle: .alert) + + let darkAction = UIAlertAction(title: "Toggle Dark UI", style: .default, handler: { _ in + WKRUIStyle.isDark = !WKRUIStyle.isDark + exit(1998) + }) + alertController.addAction(darkAction) + + let buildAction = UIAlertAction(title: "Show Build Info", style: .default, handler: { _ in + self.showDebugBuildInfo() + }) + alertController.addAction(buildAction) + + let defaultsAction = UIAlertAction(title: "Show Defaults", style: .default, handler: { _ in + self.showDebugDefaultsInfo() + }) + alertController.addAction(defaultsAction) + + alertController.addCancelAction(title: "Dismiss") + + present(alertController, animated: true, completion: nil) + } + + private func showDebugBuildInfo() { + let versionKey = "CFBundleVersion" + let shortVersionKey = "CFBundleShortVersionString" + + let appBundleInfo = Bundle.main.infoDictionary + let kitBundleInfo = Bundle(for: WKRGameManager.self).infoDictionary + let interfaceBundleInfo = Bundle(for: WKRUIStyle.self).infoDictionary + + guard let appBundleVersion = appBundleInfo?[versionKey] as? String, + let appBundleShortVersion = appBundleInfo?[shortVersionKey] as? String, + let kitBundleVersion = kitBundleInfo?[versionKey] as? String, + let kitBundleShortVersion = kitBundleInfo?[shortVersionKey] as? String, + let interfaceBundleVersion = interfaceBundleInfo?[versionKey] as? String, + let interfaceBundleShortVersion = interfaceBundleInfo?[shortVersionKey] as? String else { + fatalError("No bundle info dictionary") + } + + let debugInfoController = DebugInfoTableViewController() + debugInfoController.title = "Build Info" + debugInfoController.info = [ + ("WikiRaces Version", "\(appBundleShortVersion) (\(appBundleVersion))"), + ("WKRKit Version", "\(kitBundleShortVersion) (\(kitBundleVersion))"), + ("WKRUIKit Version", "\(interfaceBundleShortVersion) (\(interfaceBundleVersion))"), + + ("WKRKit Constants Version", "\(WKRKitConstants.current.version)"), + ("WKRUIKit Constants Version", "\(WKRUIKitConstants.current.version)") + ] + + let navController = UINavigationController(rootViewController: debugInfoController) + present(navController, animated: true, completion: nil) + } + + private func showDebugDefaultsInfo() { + let debugInfoController = DebugInfoTableViewController() + debugInfoController.title = "User Defaults" + debugInfoController.info = UserDefaults + .standard + .dictionaryRepresentation() + .sorted { (lhs, rhs) -> Bool in + return lhs.key.lowercased() < rhs.key.lowercased() + } + + let navController = UINavigationController(rootViewController: debugInfoController) + present(navController, animated: true, completion: nil) + } +} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+GameKit.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+GameKit.swift new file mode 100644 index 0000000..e9280d3 --- /dev/null +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+GameKit.swift @@ -0,0 +1,64 @@ +// +// MenuViewController+GameKit.swift +// WikiRaces +// +// Created by Andrew Finke on 9/6/17. +// Copyright © 2017 Andrew Finke. All rights reserved. +// + +import GameKit + +extension MenuViewController: GKGameCenterControllerDelegate { + + // MARK: - Game Center + + /// Attempts Game Center login + func attemptGlobalAuthentication() { + GlobalRaceHelper.shared.authenticate { controller, error, forceShowError in + if let controller = controller, self.menuView.state != .noInterface { + if self.presentedViewController == nil { + self.present(controller, animated: true, completion: nil) + } + } else if GKLocalPlayer.local.isAuthenticated { + let metrics = PlayerDatabaseMetrics.shared + metrics.log(value: GKLocalPlayer.local.alias, for: "GCAliases") + } else if !GKLocalPlayer.local.isAuthenticated { + if error != nil || forceShowError { + self.presentGameKitAuthAlert() + } + } + if let error = error { + let info = "attemptGlobalAuthentication: " + error.localizedDescription + PlayerAnonymousMetrics.log(event: .error(info)) + } + } + } + + // MARK: - GKGameCenterControllerDelegate + + func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + dismiss(animated: true) { + self.menuView.animateMenuIn() + } + } + + // MARK: - Other + + func presentGameKitAuthAlert() { + let title = "Global Races Unavailable" + let message = """ + Please try logging into Game Center in the Settings app to join a Global Race. + """ + + let controller = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + controller.addCancelAction(title: "Ok") + + if presentedViewController == nil { + present(controller, animated: true, completion: nil) + } + } + +} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift index 9994c7d..c1c357f 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+KB.swift @@ -16,16 +16,34 @@ extension MenuViewController { return [ UIKeyCommand(input: "n", modifierFlags: .command, - action: #selector(createRace), - discoverabilityTitle: "Create Race"), + action: #selector(keyboardCreateLocalRace), + discoverabilityTitle: "Create Local Race"), UIKeyCommand(input: "j", modifierFlags: .command, - action: #selector(joinRace), - discoverabilityTitle: "Join Race"), - UIKeyCommand(input: "s", + action: #selector(keyboardJoinLocalRace), + discoverabilityTitle: "Join Local Race"), + UIKeyCommand(input: "g", modifierFlags: .command, - action: #selector(openSettings), - discoverabilityTitle: "Open Settings") + action: #selector(keyboardJoinGlobalRace), + discoverabilityTitle: "Join Global Race") ] } + + @objc private func keyboardJoinLocalRace() { + PlayerAnonymousMetrics.log(event: .pressedJoin) + PlayerDatabaseStat.mpcPressedJoin.increment() + presentMPCConnect(isHost: false) + } + + @objc private func keyboardCreateLocalRace() { + PlayerAnonymousMetrics.log(event: .pressedHost) + PlayerDatabaseStat.mpcPressedHost.increment() + presentMPCConnect(isHost: true) + } + + @objc private func keyboardJoinGlobalRace() { + PlayerAnonymousMetrics.log(event: .pressedGlobalJoin) + PlayerDatabaseStat.gkPressedJoin.increment() + presentGlobalConnect() + } } diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Leaderboards.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Leaderboards.swift deleted file mode 100644 index e3b41be..0000000 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Leaderboards.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// MenuViewController+Leaderboards.swift -// WikiRaces -// -// Created by Andrew Finke on 9/6/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import GameKit - -extension MenuViewController: GKGameCenterControllerDelegate { - - // MARK: - Interface - - /// Called when a tile is pressed - /// - /// - Parameter sender: The pressed tile - @objc - func menuTilePressed(sender: MenuTile) { - PlayerMetrics.log(event: .userAction(#function)) - - guard GKLocalPlayer.local.isAuthenticated else { - attemptGCAuthentication() - return - } - guard !isLeaderboardPresented else { - return - } - - isLeaderboardPresented = true - animateMenuOut { - let controller = GKGameCenterViewController() - controller.gameCenterDelegate = self - controller.viewState = .leaderboards - controller.leaderboardTimeScope = .allTime - self.present(controller, animated: true, completion: nil) - PlayerMetrics.log(presentingOf: controller, on: self) - } - - if let leaderboard = sender.stat?.leaderboard { - PlayerMetrics.log(event: .leaderboard, attributes: ["Leaderboard": leaderboard as Any]) - } - } - - // MARK: - Game Center - - /// Attempts Game Center login - func attemptGCAuthentication() { - guard !UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") else { - return - } - - GKLocalPlayer.local.authenticateHandler = { viewController, error in - DispatchQueue.main.async { - if let viewController = viewController, self.isMenuVisable { - if self.presentedViewController == nil { - self.present(viewController, animated: true, completion: nil) - PlayerMetrics.log(presentingOf: viewController, on: self) - } - } else if !GKLocalPlayer.local.isAuthenticated { - // "error._code" ?!?! - if let error = error, error._code == 2 { - return - } - //swiftlint:disable:next line_length - let controller = UIAlertController(title: "Leaderboards Unavailable", message: "You must be logged into Game Center to access leaderboards", preferredStyle: .alert) - - let settingsAction = UIAlertAction(title: "Settings", style: .default, handler: { _ in - PlayerMetrics.log(event: .userAction("attemptGCAuthentication:settings")) - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { - fatalError("Settings URL nil") - } - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) - }) - controller.addAction(settingsAction) - controller.addCancelAction(title: "Ok") - - self.present(controller, animated: true, completion: nil) - PlayerMetrics.log(presentingOf: controller, on: self) - } - } - } - } - - // MARK: - GKGameCenterControllerDelegate - - func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) { - PlayerMetrics.log(event: .userAction(#function)) - dismiss(animated: true) { - self.animateMenuIn() - self.isLeaderboardPresented = false - } - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Segue.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Segue.swift deleted file mode 100644 index 300a5de..0000000 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+Segue.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// MenuViewController+Segue.swift -// WikiRaces -// -// Created by Andrew Finke on 9/6/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import UIKit -import WKRKit - -extension MenuViewController { - - // MARK: - Types - - enum Segue: String { - case debugBypass - case showConnecting - } - - // MARK: - Performing Segues - - /// Perform as segue with host parameter - /// - /// - Parameters: - /// - segue: The segue to perform - /// - isHost: Is the local player host - func performSegue(_ segue: Segue, isHost: Bool) { - performSegue(withIdentifier: segue.rawValue, sender: isHost) - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - guard let unwrappedSegueIdentifier = segue.identifier, - let segueIdentifier = Segue(rawValue: unwrappedSegueIdentifier), - let isPlayerHost = sender as? Bool else { - fatalError("Unknown segue \(String(describing: segue.identifier))") - } - - UIApplication.shared.isIdleTimerDisabled = true - - switch segueIdentifier { - case .debugBypass: - guard let destination = (segue.destination as? UINavigationController)? - .rootViewController as? GameViewController else { - fatalError("Destination not a GameViewController nav") - } - #if MULTIWINDOWDEBUG - //swiftlint:disable:next force_cast - destination.networkConfig = .multiwindow(multiWindowName: (view.window as! DebugWindow).playerName, - isHost: isPlayerHost) - #else - fatalError() - #endif - - case .showConnecting: - #if MULTIWINDOWDEBUG - fatalError() - #else - guard let destination = segue.destination as? MPCConnectViewController else { - fatalError("Destination not a MPCConnectViewController nav") - } - destination.isPlayerHost = isPlayerHost - #endif - } - } - - // MARK: - Settings - - @objc - func openSettings() { - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { - fatalError("Settings URL nil") - } - - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) - } - -} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+UI.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+UI.swift deleted file mode 100644 index 127cdc2..0000000 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController+UI.swift +++ /dev/null @@ -1,304 +0,0 @@ -// -// MenuViewController+UI.swift -// WikiRaces -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import UIKit -import WKRUIKit - -extension MenuViewController { - - // MARK: - Interface - - /// By WikiRaces 4 I hope there is a better way to do this - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - // Button Styles - - let buttonStyle: WKRUIButtonStyle - let buttonWidth: CGFloat - let buttonHeight: CGFloat - if view.frame.size.width > 420 { - buttonStyle = .large - buttonWidth = 195 - buttonHeight = 50 - } else { - buttonStyle = .normal - buttonWidth = 175 - buttonHeight = 40 - } - - if view.frame.size.width < UIScreen.main.bounds.width / 1.8 { - leftMenuTile?.title = "WIKI\nPOINTS" - middleMenuTile?.title = "AVG PER\nRACE" - rightMenuTile?.title = "RACES\nPLAYED" - } else { - leftMenuTile?.title = "WIKI POINTS" - middleMenuTile?.title = "AVG PER RACE" - rightMenuTile?.title = "RACES PLAYED" - } - - createButton.style = buttonStyle - joinButton.style = buttonStyle - - // Label Fonts - - titleLabel.font = UIFont.boldSystemFont(ofSize: min(view.frame.size.width / 10.0, 55)) - subtitleLabel.font = UIFont.systemFont(ofSize: min(view.frame.size.width / 18.0, 30), weight: .medium) - - // Constraints - - titleLabelConstraint.constant = view.frame.size.height / 7 - - if isMenuVisable { - topViewLeftConstraint.constant = 0 - bottomViewAnchorConstraint.constant = 0 - - if view.frame.size.height < 650 { - bottomViewAnchorConstraint.constant = 75 - } - } else { - topViewLeftConstraint.constant = -topView.frame.width - bottomViewAnchorConstraint.constant = bottomView.frame.height - } - - createButtonWidthConstraint.constant = buttonWidth + 30 - createButtonHeightConstraint.constant = buttonHeight - - joinButtonWidthConstraint.constant = buttonWidth - joinButtonHeightConstraint.constant = buttonHeight - } - - /// One-off setup - func setupInterface() { - view.backgroundColor = UIColor.wkrBackgroundColor - UIApplication.shared.keyWindow?.backgroundColor = UIColor.wkrBackgroundColor - - topView.translatesAutoresizingMaskIntoConstraints = false - topView.backgroundColor = UIColor.wkrMenuTopViewColor - view.addSubview(topView) - - bottomView.backgroundColor = UIColor.wkrMenuBottomViewColor - bottomView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(bottomView) - - topViewLeftConstraint = topView.leftAnchor.constraint(equalTo: view.leftAnchor) - bottomViewAnchorConstraint = bottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 250) - bottomViewHeightConstraint = bottomView.heightAnchor.constraint(equalToConstant: 250) - - setupTopView() - setupBottomView() - - let constraints = [ - topView.topAnchor.constraint(equalTo: view.topAnchor), - topView.bottomAnchor.constraint(equalTo: bottomView.topAnchor), - topView.widthAnchor.constraint(equalTo: view.widthAnchor), - - bottomView.leftAnchor.constraint(equalTo: view.leftAnchor), - bottomView.widthAnchor.constraint(equalTo: view.widthAnchor), - bottomViewHeightConstraint!, - - topViewLeftConstraint!, - bottomViewAnchorConstraint! - ] - NSLayoutConstraint.activate(constraints) - } - - @available(iOS 11.0, *) - override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - puzzleViewHeightConstraint.constant = 75 + view.safeAreaInsets.bottom / 2 - bottomViewHeightConstraint.constant = 250 + view.safeAreaInsets.bottom / 2 - } - - // MARK: - Top View - - /// Sets up the top view of the menu - private func setupTopView() { - setupLabels() - setupButtons() - - titleLabelConstraint = titleLabel.topAnchor.constraint(equalTo: topView.topAnchor, constant: 200) - - createButtonWidthConstraint = createButton.widthAnchor.constraint(equalToConstant: 215) - createButtonHeightConstraint = createButton.heightAnchor.constraint(equalToConstant: 45) - joinButtonWidthConstraint = joinButton.widthAnchor.constraint(equalToConstant: 185) - joinButtonHeightConstraint = joinButton.heightAnchor.constraint(equalToConstant: 45) - - let constraints = [ - titleLabelConstraint!, - - joinButtonWidthConstraint!, - joinButtonHeightConstraint!, - - createButtonWidthConstraint!, - createButtonHeightConstraint!, - - titleLabel.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 30), - titleLabel.widthAnchor.constraint(equalTo: topView.widthAnchor), - - subtitleLabel.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 30), - subtitleLabel.widthAnchor.constraint(equalTo: topView.widthAnchor), - subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), - - joinButton.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 40.0), - joinButton.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 30), - - createButton.leftAnchor.constraint(equalTo: topView.leftAnchor, constant: 30), - createButton.topAnchor.constraint(equalTo: joinButton.bottomAnchor, constant: 20.0) - ] - NSLayoutConstraint.activate(constraints) - } - - /// Sets up the buttons - private func setupButtons() { - joinButton.title = "Join race" - joinButton.translatesAutoresizingMaskIntoConstraints = false - joinButton.addTarget(self, action: #selector(joinRace), for: .touchUpInside) - topView.addSubview(joinButton) - - createButton.title = "Create race" - createButton.translatesAutoresizingMaskIntoConstraints = false - createButton.addTarget(self, action: #selector(createRace), for: .touchUpInside) - topView.addSubview(createButton) - } - - /// Sets up the labels - private func setupLabels() { - titleLabel.text = "WikiRaces" - titleLabel.textColor = UIColor.wkrTextColor - titleLabel.translatesAutoresizingMaskIntoConstraints = false - topView.addSubview(titleLabel) - - #if DEBUG - titleLabel.textColor = UIColor(red: 51.0 / 255.0, green: 102.0 / 255.0, blue: 204.0 / 255.0, alpha: 1.0) - #endif - - subtitleLabel.text = "Conquer the encyclopedia\nof everything." - subtitleLabel.numberOfLines = 2 - subtitleLabel.textColor = UIColor.wkrTextColor - subtitleLabel.translatesAutoresizingMaskIntoConstraints = false - subtitleLabel.clipsToBounds = true - topView.addSubview(subtitleLabel) - } - - // MARK: - Bottom View - - /// Sets up the bottom views - private func setupBottomView() { - let stackView = setupStatsStackView() - let puzzleView = setupPuzzleView() - puzzleViewHeightConstraint = puzzleView.heightAnchor.constraint(equalToConstant: 75) - - let constraints = [ - puzzleViewHeightConstraint!, - puzzleView.leftAnchor.constraint(equalTo: bottomView.leftAnchor), - puzzleView.rightAnchor.constraint(equalTo: bottomView.rightAnchor), - puzzleView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor), - - stackView.leftAnchor.constraint(equalTo: bottomView.leftAnchor, constant: 15), - stackView.rightAnchor.constraint(equalTo: bottomView.rightAnchor, constant: -15), - stackView.topAnchor.constraint(equalTo: bottomView.topAnchor), - stackView.bottomAnchor.constraint(equalTo: puzzleView.topAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - - /// Sets up the stack view that holds the menu tiles - //swiftlint:disable:next function_body_length - private func setupStatsStackView() -> UIStackView { - let statsStackView = UIStackView() - statsStackView.distribution = .fillEqually - statsStackView.translatesAutoresizingMaskIntoConstraints = false - bottomView.addSubview(statsStackView) - - let leftMenuTile = MenuTile(title: "WIKI POINTS") - leftMenuTile.value = StatsHelper.shared.statValue(for: .points) - statsStackView.addArrangedSubview(leftMenuTile) - - let middleMenuTile = MenuTile(title: "AVG PER RACE") - middleMenuTile.value = StatsHelper.shared.statValue(for: .average) - statsStackView.addArrangedSubview(middleMenuTile) - let leftThinLine = WKRUIThinLineView() - middleMenuTile.addSubview(leftThinLine) - let rightThinLine = WKRUIThinLineView() - middleMenuTile.addSubview(rightThinLine) - - let rightMenuTile = MenuTile(title: "RACES PLAYED") - rightMenuTile.value = StatsHelper.shared.statValue(for: .races) - statsStackView.addArrangedSubview(rightMenuTile) - - let constraints = [ - leftThinLine.leftAnchor.constraint(equalTo: middleMenuTile.leftAnchor), - leftThinLine.topAnchor.constraint(equalTo: middleMenuTile.topAnchor, constant: 30), - leftThinLine.bottomAnchor.constraint(equalTo: middleMenuTile.bottomAnchor, constant: -25), - leftThinLine.widthAnchor.constraint(equalToConstant: 2), - - rightThinLine.rightAnchor.constraint(equalTo: middleMenuTile.rightAnchor), - rightThinLine.topAnchor.constraint(equalTo: middleMenuTile.topAnchor, constant: 30), - rightThinLine.bottomAnchor.constraint(equalTo: middleMenuTile.bottomAnchor, constant: -25), - rightThinLine.widthAnchor.constraint(equalToConstant: 2) - ] - NSLayoutConstraint.activate(constraints) - - leftMenuTile.stat = .points - middleMenuTile.stat = .races - rightMenuTile.stat = .average - - leftMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) - middleMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) - rightMenuTile.addTarget(self, action: #selector(menuTilePressed(sender:)), for: .touchUpInside) - - self.leftMenuTile = leftMenuTile - self.middleMenuTile = middleMenuTile - self.rightMenuTile = rightMenuTile - - StatsHelper.shared.keyStatsUpdated = { points, races, average in - DispatchQueue.main.async { - self.leftMenuTile?.value = points - self.middleMenuTile?.value = average - self.rightMenuTile?.value = races - } - } - - if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { - self.leftMenuTile?.value = 140 - self.middleMenuTile?.value = 140/72 - self.rightMenuTile?.value = 72 - } - - return statsStackView - } - - /// Sets up the view that animates the puzzzle pieces - private func setupPuzzleView() -> UIView { - let puzzleBackgroundView = UIView() - - puzzleBackgroundView.isUserInteractionEnabled = false - puzzleBackgroundView.backgroundColor = UIColor.wkrMenuPuzzleViewColor - puzzleBackgroundView.translatesAutoresizingMaskIntoConstraints = false - bottomView.addSubview(puzzleBackgroundView) - - puzzleView.backgroundColor = UIColor(patternImage: #imageLiteral(resourceName: "MenuBackgroundPuzzle")) - puzzleView.translatesAutoresizingMaskIntoConstraints = false - puzzleBackgroundView.addSubview(puzzleView) - - let constraints = [ - puzzleView.leftAnchor.constraint(equalTo: puzzleBackgroundView.leftAnchor), - puzzleView.rightAnchor.constraint(equalTo: puzzleBackgroundView.rightAnchor), - puzzleView.topAnchor.constraint(equalTo: puzzleBackgroundView.topAnchor, constant: 22.5), - puzzleView.heightAnchor.constraint(equalToConstant: 30) - ] - NSLayoutConstraint.activate(constraints) - - return puzzleBackgroundView - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return UIStatusBarStyle.wkrStatusBarStyle - } -} diff --git a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift index 6307df3..8fdfab6 100644 --- a/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift +++ b/WikiRaces/Shared/Menu View Controllers/MenuViewController/MenuViewController.swift @@ -14,72 +14,28 @@ import WKRKit import WKRUIKit /// The main menu view controller -internal class MenuViewController: StateLogViewController { +internal class MenuViewController: UIViewController { - // MARK: - Properties + // MARK: - View - /// Used to track if the menu should be animating - var isMenuVisable = false - - var isLeaderboardPresented = false - - // MARK: - Interface Elements - - /// The top of the menu (everything on white). Animates out of the left side. - let topView = UIView() - /// The bottom of the menu (everything not white). Animates out of the bottom. - let bottomView = UIView() - - /// The "WikiRaces" label - let titleLabel = UILabel() - /// The "Conquer..." label - let subtitleLabel = UILabel() - - let joinButton = WKRUIButton() - let createButton = WKRUIButton() - - /// The Wiki Points tile - var leftMenuTile: MenuTile? - /// The average points tile - var middleMenuTile: MenuTile? - /// The races tile - var rightMenuTile: MenuTile? - - /// Timer for moving the puzzle pieces - var puzzleTimer: Timer? - /// The puzzle piece view - let puzzleView = UIScrollView() - - // MARK: - Constraints - - /// Used to animate the top view in and out - var topViewLeftConstraint: NSLayoutConstraint! - /// Used to animate the bottom view in and out - var bottomViewAnchorConstraint: NSLayoutConstraint! + override func loadView() { + view = MenuView() + } - /// Used for safe area layout adjustments - var bottomViewHeightConstraint: NSLayoutConstraint! - var puzzleViewHeightConstraint: NSLayoutConstraint! + var menuView: MenuView { + guard let view = view as? MenuView else { fatalError() } + return view + } - /// Used for adjusting y coord of title label based on screen height - var titleLabelConstraint: NSLayoutConstraint! + private var isFirstAppearence = true - /// Used for adjusting button widths and heights based on screen width - var joinButtonWidthConstraint: NSLayoutConstraint! - var joinButtonHeightConstraint: NSLayoutConstraint! - var createButtonWidthConstraint: NSLayoutConstraint! - var createButtonHeightConstraint: NSLayoutConstraint! + override var canBecomeFirstResponder: Bool { return true } + override var preferredStatusBarStyle: UIStatusBarStyle { return .wkrStatusBarStyle } // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() - setupInterface() - - let panelGesture = UITapGestureRecognizer(target: self, action: #selector(showDebugPanel)) - panelGesture.numberOfTapsRequired = 2 - panelGesture.numberOfTouchesRequired = 2 - view.addGestureRecognizer(panelGesture) //swiftlint:disable:next discarded_notification_center_observer NotificationCenter.default.addObserver(forName: NSNotification.Name.localPlayerQuit, @@ -91,240 +47,130 @@ internal class MenuViewController: StateLogViewController { }) } } - } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - animateMenuIn() + menuView.listenerUpdate = { [weak self] update in + guard let self = self else { return } + switch update { + case .presentDebug: self.presentDebugController() + case .presentGlobalConnect: self.presentGlobalConnect() + case .presentGlobalAuth: self.attemptGlobalAuthentication() + case .presentMPCConnect(let isHost): self.presentMPCConnect(isHost: isHost) + case .presentAlert(let alertController): + self.present(alertController, animated: true, completion: nil) + case .presentLeaderboard: + let controller = GKGameCenterViewController() + controller.gameCenterDelegate = self + controller.viewState = .leaderboards + controller.leaderboardTimeScope = .allTime + self.present(controller, animated: true, completion: nil) + } - #if MULTIWINDOWDEBUG - performSegue(.debugBypass, isHost: view.window!.frame.origin == .zero) - #else - attemptGCAuthentication() + } - if let name = UserDefaults.standard.object(forKey: "name_preference") as? String { - PlayerMetrics.log(event: .hasCustomName(name)) - } - if GKLocalPlayer.local.isAuthenticated { - PlayerMetrics.log(event: .hasGCAlias(GKLocalPlayer.local.alias)) + GlobalRaceHelper.shared.didReceiveInvite = { + DispatchQueue.main.async { + guard self.presentedViewController == nil else { return } + self.menuView.joinGlobalRace() } - PlayerMetrics.log(event: .hasDeviceName(UIDevice.current.name)) - #endif + } } - // MARK: - Actions + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIApplication.shared.isIdleTimerDisabled = false - /// Join button pressed - @objc - func joinRace() { - guard isMenuVisable else { return } - PlayerMetrics.log(event: .userAction(#function)) - PlayerMetrics.log(event: .pressedJoin) + // adjusts views before animation if rotation occured + menuView.setNeedsLayout() + menuView.layoutIfNeeded() - UISelectionFeedbackGenerator().selectionChanged() + menuView.animateMenuIn(completion: { + if SKStoreReviewController.shouldPromptForRating { + #if !DEBUG + SKStoreReviewController.requestReview() + #endif + } + }) - guard !promptForCustomName(isHost: false) else { - return + #if MULTIWINDOWDEBUG + let controller = GameViewController() + let nav = UINavigationController(rootViewController: controller) + let name = (view.window as? DebugWindow)?.playerName ?? "" + controller.networkConfig = .multiwindow(windowName: name, + isHost: view.window!.frame.origin == .zero) + present(nav, animated: false, completion: nil) + #else + if isFirstAppearence { + isFirstAppearence = false + attemptGlobalAuthentication() } + #endif - animateMenuOut { - self.performSegue(.showConnecting, isHost: false) + let metrics = PlayerDatabaseMetrics.shared + metrics.log(value: UIDevice.current.name, for: "DeviceNames") + if let name = UserDefaults.standard.object(forKey: "name_preference") as? String { + metrics.log(value: name, for: "CustomNames") } - } - - /// Create button pressed - @objc - func createRace() { - guard isMenuVisable else { return } - PlayerMetrics.log(event: .userAction(#function)) - PlayerMetrics.log(event: .pressedHost) - UISelectionFeedbackGenerator().selectionChanged() - - guard !promptForCustomName(isHost: true) else { - return - } + promptForInvalidName() + becomeFirstResponder() + } - animateMenuOut { - self.performSegue(.showConnecting, isHost: true) + override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + if motion == .motionShake { + menuView.triggeredEasterEgg() } } - func promptForCustomName(isHost: Bool) -> Bool { - guard !UserDefaults.standard.bool(forKey: "PromptedCustomName") else { - return false + // MARK: - Name Checking + + func promptForInvalidName() { + guard UserDefaults.standard.bool(forKey: "AttemptingMCPeerIDCreation") else { + return } - UserDefaults.standard.set(true, forKey: "PromptedCustomName") + UserDefaults.standard.set(false, forKey: "AttemptingMCPeerIDCreation") - let message = "Would you like to set a custom player name before racing?" - let alertController = UIAlertController(title: "Set Name?", message: message, preferredStyle: .alert) + //swiftlint:disable:next line_length + let message = "There was an unexpected issue starting a race with your player name. This can often occur when your name has too many emojis or too many letters. Please set a new custom player name before racing." + let alertController = UIAlertController(title: "Player Name Issue", message: message, preferredStyle: .alert) let laterAction = UIAlertAction(title: "Maybe Later", style: .cancel, handler: { _ in - PlayerMetrics.log(event: .userAction("promptForCustomNamePrompt:rejected")) - PlayerMetrics.log(event: .namePromptResult, attributes: ["Result": "Cancelled"]) - if isHost { - self.createRace() - } else { - self.joinRace() - } + PlayerAnonymousMetrics.log(event: .userAction("promptForInvalidName:rejected")) }) alertController.addAction(laterAction) - let settingsAction = UIAlertAction(title: "Open Settings", style: .default, handler: { _ in - PlayerMetrics.log(event: .userAction("promptForCustomNamePrompt:accepted")) - PlayerMetrics.log(event: .namePromptResult, attributes: ["Result": "Accepted"]) - - self.openSettings() + let settingsAction = UIAlertAction(title: "Change Name", style: .default, handler: { _ in + PlayerAnonymousMetrics.log(event: .userAction("promptForInvalidName:accepted")) + UIApplication.shared.openSettings() }) alertController.addAction(settingsAction) present(alertController, animated: true, completion: nil) - PlayerMetrics.log(presentingOf: alertController, on: self) - return true } - /// Changes title label to build info - @objc - func showDebugPanel() { - PlayerMetrics.log(event: .versionInfo) + // MARK: - Other - let message = "If your name isn't Andrew, you probably shouldn’t be here." - let alertController = UIAlertController(title: "Debug Panel", - message: message, - preferredStyle: .alert) + func presentMPCConnect(isHost: Bool) { + UIApplication.shared.isIdleTimerDisabled = true - let darkAction = UIAlertAction(title: "Toggle Dark UI", style: .default, handler: { _ in - WKRUIStyle.isDark = !WKRUIStyle.isDark - exit(1998) - }) - alertController.addAction(darkAction) - - let buildAction = UIAlertAction(title: "Show Build Info", style: .default, handler: { _ in - self.showDebugBuildInfo() - }) - alertController.addAction(buildAction) - - let defaultsAction = UIAlertAction(title: "Show Defaults", style: .default, handler: { _ in - self.showDebugDefaultsInfo() - }) - alertController.addAction(defaultsAction) - - alertController.addCancelAction(title: "Dismiss") - - present(alertController, animated: true, completion: nil) - - PlayerMetrics.log(presentingOf: alertController, on: self) - } - - private func showDebugBuildInfo() { - let versionKey = "CFBundleVersion" - let shortVersionKey = "CFBundleShortVersionString" - - let appBundleInfo = Bundle.main.infoDictionary - let kitBundleInfo = Bundle(for: WKRGameManager.self).infoDictionary - let interfaceBundleInfo = Bundle(for: WKRUIStyle.self).infoDictionary - - guard let appBundleVersion = appBundleInfo?[versionKey] as? String, - let appBundleShortVersion = appBundleInfo?[shortVersionKey] as? String, - let kitBundleVersion = kitBundleInfo?[versionKey] as? String, - let kitBundleShortVersion = kitBundleInfo?[shortVersionKey] as? String, - let interfaceBundleVersion = interfaceBundleInfo?[versionKey] as? String, - let interfaceBundleShortVersion = interfaceBundleInfo?[shortVersionKey] as? String else { - fatalError("No bundle info dictionary") - } - - let debugInfoController = DebugInfoTableViewController() - debugInfoController.title = "Build Info" - debugInfoController.info = [ - ("WikiRaces Version", "\(appBundleShortVersion) (\(appBundleVersion))"), - ("WKRKit Version", "\(kitBundleShortVersion) (\(kitBundleVersion))"), - ("WKRUIKit Version", "\(interfaceBundleShortVersion) (\(interfaceBundleVersion))"), - - ("WKRKit Constants Version", "\(WKRKitConstants.current.version)"), - ("WKRUIKit Constants Version", "\(WKRUIKitConstants.current.version)") - ] - - let navController = UINavigationController(rootViewController: debugInfoController) - present(navController, animated: true, completion: nil) - - PlayerMetrics.log(presentingOf: navController, on: self) + let controller = MPCConnectViewController() + controller.isPlayerHost = isHost + navigationController?.pushViewController(controller, animated: false) } - private func showDebugDefaultsInfo() { - let debugInfoController = DebugInfoTableViewController() - debugInfoController.title = "User Defaults" - debugInfoController.info = UserDefaults - .standard - .dictionaryRepresentation() - .sorted { (lhs, rhs) -> Bool in - return lhs.key.lowercased() < rhs.key.lowercased() + func presentGlobalConnect() { + if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + let controller = GameViewController() + let nav = UINavigationController(rootViewController: controller) + let url = URL(string: "https://en.m.wikipedia.org/wiki/Walt_Disney_World")! + controller.prepareForScreenshots(for: url) + present(nav, animated: true, completion: nil) + } else if GKLocalPlayer.local.isAuthenticated { + UIApplication.shared.isIdleTimerDisabled = true + let controller = GameKitConnectViewController() + navigationController?.pushViewController(controller, animated: false) + } else { + presentGameKitAuthAlert() } - - let navController = UINavigationController(rootViewController: debugInfoController) - present(navController, animated: true, completion: nil) - - PlayerMetrics.log(presentingOf: navController, on: self) - } - - // MARK: - Menu Animations - - /// Animates the views off screen - /// - /// - Parameter completion: The completion handler - func animateMenuOut(completion: (() -> Void)?) { - view.isUserInteractionEnabled = false - bottomViewAnchorConstraint.constant = bottomView.frame.height - - isMenuVisable = false - view.setNeedsLayout() - - UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle, animations: { - self.view.layoutIfNeeded() - }, completion: { _ in - self.puzzleTimer?.invalidate() - completion?() - }) - } - - /// Animates the views on screen - func animateMenuIn() { - view.isUserInteractionEnabled = false - UIApplication.shared.isIdleTimerDisabled = false - - let duration = TimeInterval(5) - let offset = CGFloat(40 * duration) - - func animateScroll() { - let xOffset = self.puzzleView.contentOffset.x + offset - UIView.animate(withDuration: duration, - delay: 0, - options: .curveLinear, - animations: { - self.puzzleView.contentOffset = CGPoint(x: xOffset, - y: 0) - }, completion: nil) - } - - puzzleTimer?.invalidate() - puzzleTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: true) { _ in - animateScroll() - } - puzzleTimer?.fire() - - isMenuVisable = true - view.setNeedsLayout() - - UIView.animate(withDuration: WKRAnimationDurationConstants.menuToggle, - animations: { - self.view.layoutIfNeeded() - }, completion: { _ in - self.view.isUserInteractionEnabled = true - if UserDefaults.standard.bool(forKey: "ShouldPromptForRating") { - #if !DEBUG - SKStoreReviewController.requestReview() - #endif - } - }) } } diff --git a/WikiRaces/Shared/Other/CommonExtensions.swift b/WikiRaces/Shared/Other/CommonExtensions.swift index 5c19725..3643141 100644 --- a/WikiRaces/Shared/Other/CommonExtensions.swift +++ b/WikiRaces/Shared/Other/CommonExtensions.swift @@ -7,6 +7,18 @@ // import UIKit +import StoreKit + +extension Bundle { + var appInfo: (build: Int, version: String) { + guard let bundleBuildString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, + let bundleBuild = Int(bundleBuildString), + let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { + fatalError("No bundle info dictionary") + } + return (bundleBuild, bundleVersion) + } +} extension NSNotification.Name { static let localPlayerQuit = NSNotification.Name(rawValue: "LocalPlayerQuit") @@ -60,3 +72,25 @@ extension UIView { }) } } + +extension SKStoreReviewController { + private static let shouldPromptForRatingKey = "ShouldPromptForRating" + + static var shouldPromptForRating: Bool { + get { + return UserDefaults.standard.bool(forKey: shouldPromptForRatingKey) + } + set { + UserDefaults.standard.set(newValue, forKey: shouldPromptForRatingKey) + } + } +} + +extension UIApplication { + func openSettings() { + guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { + fatalError("Settings URL nil") + } + open(settingsURL, options: [:], completionHandler: nil) + } +} diff --git a/WikiRaces/Shared/Other/DurationFormatter.swift b/WikiRaces/Shared/Other/DurationFormatter.swift deleted file mode 100644 index 8a624a5..0000000 --- a/WikiRaces/Shared/Other/DurationFormatter.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// DurationFormatter.swift -// WikiRaces -// -// Created by Andrew Finke on 8/5/17. -// Copyright © 2017 Andrew Finke. All rights reserved. -// - -import Foundation - -internal struct DurationFormatter { - private static let maxSeconds: Int = 360 - - static func string(for duration: Int?) -> String? { - guard let duration = duration else { return nil } - if duration > maxSeconds { - return (duration / 60).description + " M" - } else { - return duration.description + " S" - } - } -} diff --git a/WikiRaces/Shared/Other/GlobalRacesHelper.swift b/WikiRaces/Shared/Other/GlobalRacesHelper.swift new file mode 100644 index 0000000..d16bd37 --- /dev/null +++ b/WikiRaces/Shared/Other/GlobalRacesHelper.swift @@ -0,0 +1,50 @@ +// +// GlobalRacesHelper.swift +// WikiRaces +// +// Created by Andrew Finke on 2/23/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import GameKit + +class GlobalRaceHelper: NSObject, GKLocalPlayerListener { + + // MARK: - Properties + + static let shared = GlobalRaceHelper() + var lastInvite: GKInvite? + var isHandlerSetup = false + var didReceiveInvite: (() -> Void)? + + // MARK: - Helpers + + func authenticate(completion: ((UIViewController?, Error?, _ forceShowErrorMessage: Bool) -> Void)?) { + guard !UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") else { + return + } + + guard !isHandlerSetup else { + completion?(nil, nil, true) + return + } + isHandlerSetup = true + + GKLocalPlayer.local.authenticateHandler = { controller, error in + DispatchQueue.main.async { + completion?(controller, error, false) + if GKLocalPlayer.local.isAuthenticated { + GKLocalPlayer.local.register(self) + } + } + } + } + + // MARK: - GKLocalPlayerListener + + func player(_ player: GKPlayer, didAccept invite: GKInvite) { + PlayerDatabaseStat.gkInvitedToMatch.increment() + lastInvite = invite + didReceiveInvite?() + } +} diff --git a/WikiRaces/Shared/Other/WKRAppDelegate.swift b/WikiRaces/Shared/Other/WKRAppDelegate.swift index a635d36..e498fd9 100644 --- a/WikiRaces/Shared/Other/WKRAppDelegate.swift +++ b/WikiRaces/Shared/Other/WKRAppDelegate.swift @@ -7,6 +7,8 @@ // import UIKit +import StoreKit + import WKRKit import WKRUIKit @@ -19,17 +21,41 @@ internal class WKRAppDelegate: UIResponder, UIApplicationDelegate { WKRUIKitConstants.updateConstants() // Don't be that app that prompts people when they first open it - UserDefaults.standard.set(false, forKey: "ShouldPromptForRating") + SKStoreReviewController.shouldPromptForRating = false } func configureAppearance() { UINavigationBar.appearance().tintColor = UIColor.wkrTextColor - // UINavigationBar.appearance().barTintColor = UIColor.wkrBackgroundColor UINavigationBar.appearance().titleTextAttributes = [ - .foregroundColor: UIColor.wkrTextColor + .foregroundColor: UIColor.wkrTextColor, + .font: UIFont.systemFont(ofSize: 18, weight: .semibold) ] window?.backgroundColor = UIColor.wkrBackgroundColor + + if WKRUIStyle.isDark { + UILabel.appearance(whenContainedInInstancesOf: [UITableViewHeaderFooterView.self]).textColor = UIColor.white + } + } + + func cleanTempDirectory() { + let maxDayAge = 14.0 + let maxTimeInterval = maxDayAge * 60 * 60 + let manager = FileManager.default + do { + let path = NSTemporaryDirectory() + let contents = try manager.contentsOfDirectory(atPath: path) + for file in contents { + let filePath = path + file + let attributes = try manager.attributesOfItem(atPath: filePath) + if let date = attributes[FileAttributeKey.creationDate] as? Date, + -date.timeIntervalSinceNow > maxTimeInterval { + try manager.removeItem(atPath: filePath) + } + } + } catch { + print(error) + } } } diff --git a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift index 4ca336e..eabcd58 100644 --- a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift +++ b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+KB.swift @@ -62,13 +62,13 @@ extension GameViewController { @objc private func keyboardAttemptForfeit() { guard presentedViewController == nil else { return } - helpButtonPressed(helpBarButtonItem) + helpButtonPressed() } @objc private func keyboardAttemptQuit() { guard presentedViewController == nil else { return } - quitButtonPressed(quitBarButtonItem) + quitButtonPressed() } @objc @@ -80,7 +80,7 @@ extension GameViewController { } let script = "document.getElementsByClassName('section-heading')[\(index - 1)].click()" - webView.evaluateJavaScript(script, completionHandler: nil) + webView?.evaluateJavaScript(script, completionHandler: nil) } } diff --git a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift index 83db33f..91f245d 100644 --- a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift +++ b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController+Manager.swift @@ -6,7 +6,6 @@ // Copyright © 2017 Andrew Finke. All rights reserved. // -import Foundation import Foundation import WKRKit @@ -15,94 +14,104 @@ extension GameViewController { // MARK: - WKRGameManager func setupGameManager() { - gameManager = WKRGameManager(networkConfig: networkConfig, stateUpdate: { [weak self] state, error in - if let error = error { - DispatchQueue.main.async { - self?.errorOccurred(error) - } - } else { - PlayerMetrics.log(event: .gameState("Transition: \(state).")) - self?.transition(to: state) - } - }, pointsUpdate: { [weak self] points in - if let timeRaced = self?.timeRaced { - var isSolo = false - if case .solo? = self?.networkConfig { - isSolo = true - } - StatsHelper.shared.completedRace(points: points, timeRaced: timeRaced, isSolo: isSolo) - } - }, linkCountUpdate: { [weak self] linkCount in - self?.webView.text = linkCount.description - }, logEvent: { [weak self] event, attributes in - #if !MULTIWINDOWDEBUG - guard let eventType = PlayerMetrics.Event(rawValue: event) else { - fatalError("Invalid event " + event) - } - if eventType == .pageView { - var isSolo = false - if case .solo? = self?.networkConfig { - isSolo = true - } - StatsHelper.shared.viewedPage(isSolo: isSolo) - } - PlayerMetrics.log(event: eventType, attributes: attributes) - #endif + gameManager = WKRGameManager(networkConfig: networkConfig, + gameUpdate: { [weak self] gameUpdate in + self?.gameUpdate(gameUpdate) + }, votingUpdate: { [weak self] votingUpdate in + self?.votingUpdate(votingUpdate) + }, resultsUpdate: { [weak self] resultsUpdate in + self?.resultsUpdate(resultsUpdate) }) - - configureManagerControllerClosures() } - private func configureManagerControllerClosures() { - gameManager.voting(timeUpdate: { [weak self] time in - self?.votingViewController?.voteTimeRemaing = time + private func gameUpdate(_ gameUpdate: WKRGameManager.GameUpdate) { + switch gameUpdate { + case .state(let state): + PlayerAnonymousMetrics.log(event: .gameState("Transition: \(state).")) + transition(to: state) + case .error(let error): + DispatchQueue.main.async { + self.errorOccurred(error) + } + case .log(let event): + logEvent(event) + case .playerRaceLinkCountForCurrentRace(let linkCount): + webView?.text = linkCount.description + case .playerStatsForLastRace(let points, let place, let webViewPixelsScrolled): + guard let raceType = statRaceType else { return } + PlayerStatsManager.shared.completedRace(type: raceType, + points: points, + place: place, + timeRaced: timeRaced, + pixelsScrolled: webViewPixelsScrolled) - if self?.networkConfig.isHost ?? false && time == 0, let votingInfo = self?.gameManager.voteInfo { - for index in 0.. Void)?) { - if let activeViewController = activeViewController, activeViewController.view.window != nil { - let controller: UIViewController? - if activeViewController.presentingViewController == self { - controller = activeViewController + func done() { + resetActiveControllers() + completion?() + } + if let activeViewController = activeViewController { + let viewWindow = activeViewController.view.window + let presentedViewWindow = activeViewController.presentedViewController?.view.window + if viewWindow != nil || presentedViewWindow != nil { + dismiss(animated: true, completion: { + done() + }) } else { - controller = activeViewController.presentingViewController + done() } - controller?.dismiss(animated: true, completion: { - self.resetActiveControllers() - completion?() - return - }) } else { - resetActiveControllers() - completion?() + done() } } + // MARK: - Transitions + private func transition(to state: WKRGameState) { guard !isPlayerQuitting, state != gameState else { return } gameState = state switch state { case .voting: - self.title = "" - dismissActiveController(completion: { - self.performSegue(.showVoting) - }) - navigationItem.leftBarButtonItem = nil - navigationItem.rightBarButtonItem = nil + transitionToVoting() case .results, .hostResults, .points: - raceTimer?.invalidate() - if activeViewController != resultsViewController || resultsViewController == nil { - dismissActiveController(completion: { - self.performSegue(.showResults) - UIView.animate(withDuration: WKRAnimationDurationConstants.gameFadeOut, - delay: WKRAnimationDurationConstants.gameFadeOutDelay, - options: .beginFromCurrentState, - animations: { - self.webView.alpha = 0.0 - }, completion: nil) - }) - } else { - resultsViewController?.state = state - } - navigationItem.leftBarButtonItem = nil - navigationItem.rightBarButtonItem = nil + transitionToResults() + case .race: + transitionToRace() + default: break + } + } - if state == .hostResults && networkConfig.isHost { - PlayerMetrics.log(event: .hostEndedRace) + private func transitionToVoting() { + self.title = "" + navigationController?.navigationBar.isHidden = true + dismissActiveController(completion: { [weak self] in + self?.showVotingController() + }) + navigationItem.leftBarButtonItem = nil + navigationItem.rightBarButtonItem = nil + setupNewWebView() + } + + private func showVotingController() { + let controller = VotingViewController() + controller.voteInfo = gameManager.voteInfo + controller.quitAlertController = quitAlertController(raceStarted: false) + controller.listenerUpdate = { [weak self] update in + guard let self = self else { return } + switch update { + case .voted(let page): + self.gameManager.player(.voted(page)) + // capitalized to keep consistent with past analytics + PlayerAnonymousMetrics.log(event: .voted, + attributes: ["Page": page.title?.capitalized as Any]) + + if let raceType = self.statRaceType { + var stat = PlayerDatabaseStat.mpcVotes + switch raceType { + case .mpc: stat = .mpcVotes + case .gameKit: stat = .gkVotes + case .solo: stat = .soloVotes + } + stat.increment() + } + case .quit: + self.playerQuit() } - case .race: - timeRaced = 0 - raceTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in - self?.timeRaced += 1 - }) + } - navigationController?.setNavigationBarHidden(false, animated: false) + self.votingViewController = controller + + let navController = UINavigationController(rootViewController: controller) + navController.modalTransitionStyle = .crossDissolve + navController.modalPresentationStyle = .overCurrentContext + present(navController, animated: true) { [weak self] in + self?.connectingLabel.alpha = 0.0 + self?.activityIndicatorView.alpha = 0.0 + } + } - navigationItem.leftBarButtonItem = helpBarButtonItem - navigationItem.rightBarButtonItem = quitBarButtonItem + private func transitionToResults() { + raceTimer?.invalidate() + if activeViewController != resultsViewController || resultsViewController == nil { + dismissActiveController(completion: { [weak self] in + self?.showResultsController() + UIView.animate(withDuration: WKRAnimationDurationConstants.gameFadeOut, + delay: WKRAnimationDurationConstants.gameFadeOutDelay, + options: .beginFromCurrentState, + animations: { + self?.webView?.alpha = 0.0 + }, completion: { [weak self] _ in + self?.title = nil + self?.navigationController?.setNavigationBarHidden(true, animated: false) + }) + }) + } else { + resultsViewController?.state = gameState + } + navigationItem.leftBarButtonItem = nil + navigationItem.rightBarButtonItem = nil - connectingLabel.alpha = 0.0 - activityIndicatorView.alpha = 0.0 + if gameState == .hostResults && networkConfig.isHost { + PlayerAnonymousMetrics.log(event: .hostEndedRace) + } + } - dismissActiveController(completion: nil) + private func showResultsController() { + let controller = ResultsViewController() + controller.localPlayer = gameManager.localPlayer + controller.addPlayersViewController = gameManager.hostNetworkInterface() + controller.state = gameManager.gameState + controller.resultsInfo = gameManager.hostResultsInfo + controller.isPlayerHost = networkConfig.isHost + controller.quitAlertController = quitAlertController(raceStarted: false) - if networkConfig.isHost { - PlayerMetrics.log(event: .hostStartedRace, attributes: ["Page": self.finalPage?.title as Any]) + controller.listenerUpdate = { [weak self] update in + guard let self = self else { return } + switch update { + case .readyButtonPressed: + self.gameManager.player(.ready) + case .quit: + self.playerQuit() } - default: break + } + + self.resultsViewController = controller + + let navController = UINavigationController(rootViewController: controller) + navController.modalTransitionStyle = .crossDissolve + navController.modalPresentationStyle = .overCurrentContext + present(navController, animated: true) { [weak self] in + self?.connectingLabel.alpha = 0.0 + self?.activityIndicatorView.alpha = 0.0 } } + private func transitionToRace() { + navigationController?.navigationBar.isHidden = false + timeRaced = 0 + raceTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in + self?.timeRaced += 1 + }) + + navigationController?.setNavigationBarHidden(false, animated: false) + + navigationItem.leftBarButtonItem = helpBarButtonItem + navigationItem.rightBarButtonItem = quitBarButtonItem + + connectingLabel.alpha = 0.0 + activityIndicatorView.alpha = 0.0 + + dismissActiveController(completion: nil) + + if networkConfig.isHost { + PlayerAnonymousMetrics.log(event: .hostStartedRace, + attributes: ["Page": finalPage?.title as Any]) + } + } + + // MARK: - Log Final Votes + + private func logFinalVotes() { + guard networkConfig.isHost, let votingInfo = gameManager.voteInfo else { return } + for index in 0.. UIAlertController { - var message = "Are you sure you want to quit? You will be disconnected and returned to the menu." - if raceStarted { - message += " Press the forfeit button to give up the race but stay in the match." - } + let message = "Are you sure you want to leave the match?" - let alertController = UIAlertController(title: "Return to Menu?", message: message, preferredStyle: .alert) + let alertController = UIAlertController(title: "Leave the Match?", + message: message, + preferredStyle: .alert) alertController.addCancelAction(title: "Keep Playing") if raceStarted { let forfeitAction = UIAlertAction(title: "Forfeit Race", style: .default) { [weak self] _ in - PlayerMetrics.log(event: .userAction("quitAlertController:forfeit")) - PlayerMetrics.log(event: .forfeited, attributes: ["Page": self?.finalPage?.title as Any]) + PlayerAnonymousMetrics.log(event: .userAction("quitAlertController:forfeit")) + PlayerAnonymousMetrics.log(event: .forfeited, attributes: ["Page": self?.finalPage?.title as Any]) self?.gameManager.player(.forfeited) } alertController.addAction(forfeitAction) + + let reloadAction = UIAlertAction(title: "Reload Page", style: .default) { _ in + PlayerAnonymousMetrics.log(event: .userAction("quitAlertController:reload")) + PlayerAnonymousMetrics.log(event: .usedReload) + self.webView?.reload() + } + alertController.addAction(reloadAction) } - let quitAction = UIAlertAction(title: "Return to Menu", style: .destructive) { [weak self] _ in - PlayerMetrics.log(event: .userAction("quitAlertController:quit")) - PlayerMetrics.log(event: .quitRace, attributes: ["View": self?.activeViewController?.description as Any]) + let quitAction = UIAlertAction(title: "Leave Match", style: .destructive) { [weak self] _ in + PlayerAnonymousMetrics.log(event: .userAction("quitAlertController:quit")) + PlayerAnonymousMetrics.log(event: .quitRace, attributes: nil) self?.playerQuit() } alertController.addAction(quitAction) @@ -116,7 +170,8 @@ extension GameViewController { self.isPlayerQuitting = true self.resetActiveControllers() self.gameManager.player(.quit) - NotificationCenter.default.post(name: NSNotification.Name.localPlayerQuit, object: nil) + NotificationCenter.default.post(name: NSNotification.Name.localPlayerQuit, + object: nil) } } diff --git a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController.swift b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController.swift index 105cac4..434908c 100644 --- a/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/GameViewController/GameViewController.swift @@ -12,12 +12,12 @@ import MultipeerConnectivity import WKRKit import WKRUIKit -internal class GameViewController: StateLogViewController { +internal class GameViewController: UIViewController { // MARK: - Game Properties var isPlayerQuitting = false - var isInterfaceConfigured = false + var isConfigured = false var timeRaced = 0 var raceTimer: Timer? @@ -32,17 +32,21 @@ internal class GameViewController: StateLogViewController { var gameManager: WKRGameManager! var networkConfig: WKRPeerNetworkConfig! + var statRaceType: PlayerStatsManager.RaceType? { + return PlayerStatsManager.RaceType(networkConfig) + } + // MARK: - User Interface - let webView = WKRUIWebView() + var webView: WKRUIWebView? let progressView = WKRUIProgressView() let navigationBarBottomLine = UIView() var helpBarButtonItem: UIBarButtonItem! var quitBarButtonItem: UIBarButtonItem! - @IBOutlet weak var connectingLabel: UILabel! - @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView! + let connectingLabel = UILabel() + let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) // MARK: - View Controllers @@ -61,66 +65,137 @@ internal class GameViewController: StateLogViewController { override func viewDidLoad() { super.viewDidLoad() - setupGameManager() + + if !UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + setupGameManager() + } + setupInterface() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if !isInterfaceConfigured { - isInterfaceConfigured = true - if !networkConfig.isHost { - UIView.animate(withDuration: 0.5, animations: { - self.connectingLabel.alpha = 1.0 - self.activityIndicatorView.alpha = 1.0 - }) + + if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + webView?.alpha = 1.0 + return + } + + if !isConfigured { + isConfigured = true + initalConfiguration() + } + + if gameManager.gameState == .preMatch && networkConfig.isHost, let config = networkConfig { + gameManager.player(.startedGame) + switch config { + case .solo: + PlayerAnonymousMetrics.log(event: .hostStartedMatch, attributes: nil) + case .gameKit(let match, _): + PlayerAnonymousMetrics.log(event: .hostStartedMatch, + attributes: ["ConnectedPeers": match.players.count - 1]) + case .mpc(_, let session, _): + PlayerAnonymousMetrics.log(event: .hostStartedMatch, + attributes: ["ConnectedPeers": session.connectedPeers.count]) + default: break } + } + } - if case let .mpc(_, session, _)? = networkConfig { - // Due to low usage, not accounting for players joining mid session - let playerNames = session.connectedPeers.filter({ peerID -> Bool in - return peerID != session.myPeerID - }).map({ peerID -> String in - return peerID.displayName - }) - StatsHelper.shared.connected(to: playerNames) + private func initalConfiguration() { + let logEvents: [WKRLogEvent] + if networkConfig.isHost { + if case .solo? = networkConfig { + logEvents = WKRSeenFinalArticlesStore.localLogEvents() + } else { + logEvents = WKRSeenFinalArticlesStore.hostLogEvents() } + } else { + UIView.animate(withDuration: 0.5, animations: { + self.connectingLabel.alpha = 1.0 + self.activityIndicatorView.alpha = 1.0 + }) + logEvents = WKRSeenFinalArticlesStore.localLogEvents() } - if gameManager.gameState == .preMatch && networkConfig.isHost { - gameManager.player(.startedGame) - if case let .mpc(_, session, _)? = networkConfig { - PlayerMetrics.log(event: .hostStartedMatch, - attributes: ["ConnectedPeers": session.connectedPeers.count]) + logEvents.forEach { logEvent($0) } + + guard let config = networkConfig else { return } + switch config { + case .solo: + PlayerStatsManager.shared.connected(to: [], raceType: .solo) + case .gameKit(let match, _): + let playerNames = match.players.map { player -> String in + return player.alias } + PlayerStatsManager.shared.connected(to: playerNames, raceType: .gameKit) + case .mpc(_, let session, _): + // Due to low usage, not accounting for players joining mid session + let playerNames = session.connectedPeers.map { peerID -> String in + return peerID.displayName + } + PlayerStatsManager.shared.connected(to: playerNames, raceType: .mpc) + default: + break } } // MARK: - User Actions - @IBAction func helpButtonPressed(_ sender: Any) { - PlayerMetrics.log(event: .userAction(#function)) + @objc + func helpButtonPressed() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) showHelp() } - @IBAction func quitButtonPressed(_ sender: Any) { - PlayerMetrics.log(event: .userAction(#function)) + @objc + func quitButtonPressed() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) let alertController = quitAlertController(raceStarted: true) present(alertController, animated: true, completion: nil) self.alertController = alertController - PlayerMetrics.log(presentingOf: alertController, on: self) } func showHelp() { - PlayerMetrics.log(event: .userAction("flagButtonPressed:help")) - PlayerMetrics.log(event: .usedHelp, attributes: ["Page": self.finalPage?.title as Any]) gameManager.player(.neededHelp) - performSegue(.showHelp) + + let controller = HelpViewController() + controller.url = gameManager.finalPageURL + controller.linkTapped = { [weak self] in + self?.gameManager.enqueue(message: "Links disabled in help", + duration: 2.0, + isRaceSpecific: true, + playHaptic: true) + } + self.activeViewController = controller + + let navController = UINavigationController(rootViewController: controller) + navController.modalPresentationStyle = .formSheet + present(navController, animated: true, completion: nil) + + PlayerAnonymousMetrics.log(event: .userAction("flagButtonPressed:help")) + PlayerAnonymousMetrics.log(event: .usedHelp, + attributes: ["Page": self.finalPage?.title as Any]) + if let raceType = statRaceType { + let stat: PlayerDatabaseStat + switch raceType { + case .mpc: stat = .mpcHelp + case .gameKit: stat = .gkHelp + case .solo: stat = .soloHelp + } + stat.increment() + } } func reloadPage() { - PlayerMetrics.log(event: .userAction("flagButtonPressed:reload")) - self.webView.reload() + PlayerAnonymousMetrics.log(event: .userAction("flagButtonPressed:reload")) + self.webView?.reload() + } + + // Used for screenshots / fastlane + func prepareForScreenshots(for url: URL) { + webView?.load(URLRequest(url: url)) + title = "Star Wars: Galaxy's Edge".uppercased() } } diff --git a/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryTableViewCell.swift b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryTableViewCell.swift index d5daed4..a673b5d 100644 --- a/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryTableViewCell.swift +++ b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryTableViewCell.swift @@ -46,8 +46,7 @@ internal class HistoryTableViewCell: UITableViewCell { super.init(style: style, reuseIdentifier: reuseIdentifier) tintColor = UIColor.wkrTextColor - selectionStyle = .none - backgroundColor = UIColor.clear + backgroundColor = UIColor.wkrBackgroundColor pageLabel.textColor = UIColor.wkrTextColor pageLabel.textAlignment = .left @@ -59,7 +58,7 @@ internal class HistoryTableViewCell: UITableViewCell { linkHereLabel.text = "Link Here" linkHereLabel.textColor = UIColor.lightGray linkHereLabel.textAlignment = .left - linkHereLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium) + linkHereLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium) linkHereLabel.numberOfLines = 1 linkHereLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(linkHereLabel) @@ -77,7 +76,7 @@ internal class HistoryTableViewCell: UITableViewCell { activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false addSubview(activityIndicatorView) - setupContraints() + setupConstraints() } required init?(coder aDecoder: NSCoder) { @@ -86,7 +85,7 @@ internal class HistoryTableViewCell: UITableViewCell { // MARK: - Constraints - private func setupContraints() { + private func setupConstraints() { let leftMarginConstraint = NSLayoutConstraint(item: pageLabel, attribute: .left, relatedBy: .equal, diff --git a/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryTableViewStatsCell.swift b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryTableViewStatsCell.swift new file mode 100644 index 0000000..0770078 --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryTableViewStatsCell.swift @@ -0,0 +1,39 @@ +// +// HistoryTableViewStatsCell.swift +// WikiRaces +// +// Created by Andrew Finke on 2/28/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit + +internal class HistoryTableViewStatsCell: UITableViewCell { + + // MARK: - Properties + + var stat: (key: String, value: String)? { + didSet { + textLabel?.text = stat?.key + detailTextLabel?.text = stat?.value + } + } + + static let reuseIdentifier = "statsReuseIdentifier" + + // MARK: - Initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .value1, reuseIdentifier: reuseIdentifier) + + textLabel?.textColor = .wkrTextColor + detailTextLabel?.textColor = .wkrTextColor + detailTextLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium) + isUserInteractionEnabled = false + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift index 6a2a537..8c8bd58 100644 --- a/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/HistoryViewController/HistoryViewController.swift @@ -10,59 +10,23 @@ import UIKit import WKRKit import WKRUIKit -internal class HistoryViewController: StateLogTableViewController { +import SafariServices + +internal class HistoryViewController: UITableViewController, SFSafariViewControllerDelegate { // MARK: - Properties + private var isUserScrolling = false + private var isTableViewAnimating = false + + private var deferredUpdate = false + private var entries = [WKRHistoryEntry]() - private var currentPlayerState = WKRPlayerState.connecting + private var stats: WKRPlayerRaceStats? var player: WKRPlayer? { didSet { - guard let player = player, let history = player.raceHistory else { - entries = [] - currentPlayerState = .connecting - tableView.reloadData() - return - } - - title = player.name - - guard player == oldValue else { - currentPlayerState = player.state - entries = player.raceHistory?.entries ?? [] - tableView.reloadSections(IndexSet(integer: 0), with: .fade) - return - } - - var rowsToReload = [IndexPath]() - var rowsToInsert = [IndexPath]() - - if player.state != currentPlayerState { - currentPlayerState = player.state - rowsToReload.append(IndexPath(row: history.entries.count - 1)) - } - - for (index, entry) in history.entries.enumerated() { - if index < entries.count { - if entry != entries[index] { - entries[index] = entry - rowsToReload.append(IndexPath(row: index)) - } - } else { - entries.insert(entry, at: index) - rowsToInsert.append(IndexPath(row: index)) - } - } - - let adjustedRowsToReload = rowsToReload.filter { indexPath -> Bool in - return !rowsToInsert.contains(indexPath) - } - - tableView.performBatchUpdates({ - tableView.reloadRows(at: adjustedRowsToReload, with: .fade) - tableView.insertRows(at: rowsToInsert, with: .fade) - }, completion: nil) + updateEntries(oldPlayer: oldValue) } } @@ -71,50 +35,233 @@ internal class HistoryViewController: StateLogTableViewController { override func viewDidLoad() { super.viewDidLoad() + var frame = CGRect.zero + frame.size.height = .leastNormalMagnitude + tableView.tableHeaderView = UIView(frame: frame) + navigationController?.navigationBar.barStyle = .wkrStyle - tableView.backgroundColor = UIColor.wkrBackgroundColor tableView.estimatedRowHeight = 150 tableView.rowHeight = UITableView.automaticDimension tableView.register(HistoryTableViewCell.self, forCellReuseIdentifier: HistoryTableViewCell.reuseIdentifier) + tableView.register(HistoryTableViewStatsCell.self, + forCellReuseIdentifier: HistoryTableViewStatsCell.reuseIdentifier) + + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, + target: self, + action: #selector(doneButtonPressed)) + } + + // MARK: - Logic + + // Update the table, with the goal of only updating the changed entries + // 1. Make sure the player is the same as the currently displayed one (else update the whole table) + // 2. Make sure the player and their history not nil (else ...) + // 3. Make sure something has changed + // 4. Make sure the controller is visable (else ...) + // 5. Make sure we aren't animating and the user isn't scrolling, otherwise the update will look poor (else ...) + // 6. Make sure that the new player object has more history entries (else ...) + // 7. Make sure we have the correct amount of new cells to insert (else ...) + // 8. Check if we have the same number of stats, if yes, don't use a table animation to update them + //swiftlint:disable:next function_body_length cyclomatic_complexity + private func updateEntries(oldPlayer: WKRPlayer?) { + title = player?.name + + // New Player + if let oldPlayer = oldPlayer, oldPlayer != player { + entries = player?.raceHistory?.entries ?? [] + stats = player?.stats + tableView.reloadData() + return + } + + guard let player = player, let history = player.raceHistory else { + entries = [] + stats = nil + tableView.reloadData() + return + } + + // A different player was updated in the results info object, nothing has changed + if history.entries == entries && oldPlayer?.state == player.state { + return + } + + if view.window == nil { + self.entries = history.entries + stats = player.stats + tableView.reloadData() + return + } else if isUserScrolling || isTableViewAnimating { + deferredUpdate = true + return + } + isTableViewAnimating = true + deferredUpdate = false + + var rowsToReload = [IndexPath]() + var rowsToInsert = [IndexPath]() + + if !entries.isEmpty { + rowsToReload.append(IndexPath(row: entries.count - 1)) + } + + let newEntryCount = history.entries.count - entries.count + // got an older history object, reset table + if newEntryCount < 0 { + self.entries = history.entries + stats = player.stats + tableView.reloadData() + return + } + + let startIndex = entries.count + let endIndex = startIndex + newEntryCount + for index in startIndex.. Int { + if player?.stats == nil { + return 1 + } + return 2 + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return section == 0 ? "Tap an article to view on Wikipedia" : nil + } + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return entries.count + return section == 0 ? entries.count : (stats?.raw.count ?? 0) + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return indexPath.section == 0 ? super.tableView(tableView, heightForRowAt: indexPath) : 40 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 1 { + guard let cell = tableView.dequeueReusableCell(withIdentifier: HistoryTableViewStatsCell.reuseIdentifier, + for: indexPath) as? HistoryTableViewStatsCell else { + fatalError("Unable to create cell") + } + + cell.stat = stats?.raw[indexPath.row] + return cell + } guard let cell = tableView.dequeueReusableCell(withIdentifier: HistoryTableViewCell.reuseIdentifier, for: indexPath) as? HistoryTableViewCell else { - fatalError("Unable to create cell") + fatalError("Unable to create cell") } + let playerState = player?.state ?? .connecting + let entry = entries[indexPath.row] cell.pageLabel.text = entry.page.title ?? "Unknown Page" cell.isLinkHere = entry.linkHere - cell.isShowingActivityIndicatorView = false - if let duration = DurationFormatter.string(for: entry.duration) { + if let duration = WKRDurationFormatter.string(for: entry.duration) { cell.detailLabel.text = duration - cell.detailLabel.font = UIFont.systemFont(ofSize: 17, weight: .regular) - } else if currentPlayerState == .racing { + cell.detailLabel.font = UIFont.systemFont(ofSize: 18, weight: .regular) + cell.isShowingActivityIndicatorView = false + } else if playerState == .racing { + cell.detailLabel.text = "" cell.isShowingActivityIndicatorView = true } else { - cell.detailLabel.text = currentPlayerState.text - cell.detailLabel.font = UIFont.systemFont(ofSize: 17, weight: .medium) + cell.detailLabel.text = playerState.text + cell.detailLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium) + cell.isShowingActivityIndicatorView = false } return cell } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.row < entries.count else { return } + let entry = entries[indexPath.row] + + let controller = SFSafariViewController(url: entry.page.url) + controller.delegate = self + controller.preferredControlTintColor = UIColor.wkrTextColor + if UIDevice.current.userInterfaceIdiom == .pad { + controller.modalPresentationStyle = .overFullScreen + } + present(controller, animated: true, completion: nil) + + PlayerAnonymousMetrics.log(event: .openedHistorySF) + } + + // MARK: - SFSafariViewControllerDelegate + + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + guard let indexPath = tableView.indexPathForSelectedRow else { return } + tableView.deselectRow(at: indexPath, animated: true) + } + } diff --git a/WikiRaces/Shared/Race View Controllers/Other/CenteredTableViewController.swift b/WikiRaces/Shared/Race View Controllers/Other/CenteredTableViewController.swift index 2b7daf9..5ffc8d1 100644 --- a/WikiRaces/Shared/Race View Controllers/Other/CenteredTableViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/Other/CenteredTableViewController.swift @@ -9,7 +9,7 @@ import UIKit import WKRUIKit -internal class CenteredTableViewController: StateLogViewController { +internal class CenteredTableViewController: UIViewController { // MARK: - Properties @@ -75,14 +75,14 @@ internal class CenteredTableViewController: StateLogViewController { guideLabel.textAlignment = .center guideLabel.textColor = UIColor.wkrLightTextColor - guideLabel.font = UIFont.systemFont(ofSize: 16.0, weight: .regular) + guideLabel.font = UIFont.systemFont(ofSize: 18.0, weight: .medium) guideLabel.adjustsFontSizeToFitWidth = true guideLabel.translatesAutoresizingMaskIntoConstraints = false visualEffectView.contentView.addSubview(guideLabel) descriptionLabel.textAlignment = .center descriptionLabel.textColor = UIColor.wkrTextColor - descriptionLabel.font = UIFont(monospaceSize: 20.0) + descriptionLabel.font = UIFont(monospaceSize: 20, weight: .medium) descriptionLabel.adjustsFontSizeToFitWidth = true descriptionLabel.translatesAutoresizingMaskIntoConstraints = false visualEffectView.contentView.addSubview(descriptionLabel) @@ -168,7 +168,6 @@ internal class CenteredTableViewController: StateLogViewController { tableView.dataSource = controller } - @available(iOS 11.0, *) override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() descriptionLabelBottomConstraint.constant = -view.safeAreaInsets.bottom diff --git a/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift b/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift index 5c1723c..5c2c224 100644 --- a/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/Other/HelpViewController.swift @@ -10,37 +10,64 @@ import UIKit import WebKit import WKRUIKit -internal class HelpViewController: StateLogViewController, WKNavigationDelegate { +internal class HelpViewController: UIViewController, WKNavigationDelegate { // MARK: - Properties var url: URL? var linkTapped: (() -> Void)? + let webView = WKRUIWebView() + let progressView = WKRUIProgressView() + // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() + title = "HELP" view.backgroundColor = UIColor.wkrBackgroundColor navigationController?.navigationBar.barStyle = UIBarStyle.wkrStyle navigationController?.view.backgroundColor = UIColor.wkrBackgroundColor - let webView = WKRUIWebView() + webView.text = "" webView.navigationDelegate = self - view = webView + webView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(webView) + + progressView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(progressView) + webView.progressView = progressView + + let constraints: [NSLayoutConstraint] = [ + webView.topAnchor.constraint(equalTo: view.topAnchor), + webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + webView.leftAnchor.constraint(equalTo: view.leftAnchor), + webView.rightAnchor.constraint(equalTo: view.rightAnchor), + + progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + progressView.leftAnchor.constraint(equalTo: view.leftAnchor), + progressView.rightAnchor.constraint(equalTo: view.rightAnchor), + progressView.heightAnchor.constraint(equalToConstant: 6) + ] + NSLayoutConstraint.activate(constraints) guard let url = url else { return // When would this happen? } + webView.startedPageLoad() webView.load(URLRequest(url: url)) + + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, + target: self, + action: #selector(doneButtonPressed)) } // MARK: - Actions @IBAction func doneButtonPressed() { - PlayerMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .userAction(#function)) presentingViewController?.dismiss(animated: true, completion: nil) } @@ -57,4 +84,8 @@ internal class HelpViewController: StateLogViewController, WKNavigationDelegate } } + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.webView.completedPageLoad() + } + } diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer+Creation.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer+Creation.swift new file mode 100644 index 0000000..b259c8a --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer+Creation.swift @@ -0,0 +1,311 @@ +// +// ResultRenderer+Creation.swift +// WikiRaces +// +// Created by Andrew Finke on 2/4/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import WKRKit + +//swiftlint:disable function_body_length cyclomatic_complexity +extension ResultRenderer { + + // MARK: - Section Creation + + func createHeaderView(title: String) -> UIView { + let headerView = UIView() + headerView.translatesAutoresizingMaskIntoConstraints = false + + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 18, weight: .bold) + label.text = title.uppercased() + label.textAlignment = .left + label.textColor = tintColor + label.numberOfLines = 0 + headerView.addSubview(label) + + let lineView = UIView() + lineView.translatesAutoresizingMaskIntoConstraints = false + lineView.backgroundColor = tintColor + lineView.layer.cornerRadius = 2 + headerView.addSubview(lineView) + + let constraints = [ + label.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 25), + label.leftAnchor.constraint(equalTo: headerView.leftAnchor), + label.rightAnchor.constraint(equalTo: headerView.rightAnchor), + + lineView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 5), + lineView.leftAnchor.constraint(equalTo: headerView.leftAnchor), + lineView.rightAnchor.constraint(equalTo: headerView.rightAnchor), + lineView.heightAnchor.constraint(equalToConstant: 4), + + headerView.bottomAnchor.constraint(equalTo: lineView.bottomAnchor, constant: 0) + ] + NSLayoutConstraint.activate(constraints) + + return headerView + } + + func createBannerView() -> UIView { + let bannerView = UIView() + bannerView.translatesAutoresizingMaskIntoConstraints = false + + let innerBannerView = UIView() + innerBannerView.translatesAutoresizingMaskIntoConstraints = false + bannerView.addSubview(innerBannerView) + + let imageView = UIImageView(image: UIImage(named: "PreviewIcon")!) + imageView.translatesAutoresizingMaskIntoConstraints = false + innerBannerView.addSubview(imageView) + + let titleLabel = UILabel() + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = UIFont.systemFont(ofSize: 28, weight: .semibold) + titleLabel.text = "WikiRaces 3" + titleLabel.textColor = tintColor + innerBannerView.addSubview(titleLabel) + + let detailLabel = UILabel() + detailLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium) + detailLabel.text = "RACE RESULTS" + detailLabel.textColor = .darkGray + innerBannerView.addSubview(detailLabel) + + let constraints = [ + innerBannerView.topAnchor.constraint(equalTo: bannerView.topAnchor, constant: 10), + innerBannerView.bottomAnchor.constraint(equalTo: bannerView.bottomAnchor), + innerBannerView.centerXAnchor.constraint(equalTo: bannerView.centerXAnchor, constant: -10), + innerBannerView.widthAnchor.constraint(equalToConstant: 250), + + imageView.leftAnchor.constraint(equalTo: innerBannerView.leftAnchor), + imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), + imageView.heightAnchor.constraint(equalTo: innerBannerView.heightAnchor), + + titleLabel.topAnchor.constraint(equalTo: imageView.topAnchor, constant: 10), + titleLabel.rightAnchor.constraint(equalTo: innerBannerView.rightAnchor), + + detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 0), + detailLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor) + ] + NSLayoutConstraint.activate(constraints) + + return bannerView + } + + func createRankingView(for results: WKRResultsInfo, localPlayer: WKRPlayer) -> UIView { + let rankingView = UIView() + rankingView.translatesAutoresizingMaskIntoConstraints = false + + let innerRankingsView = UIView() + innerRankingsView.translatesAutoresizingMaskIntoConstraints = false + rankingView.addSubview(innerRankingsView) + + let entrySpacing = CGFloat(15) + var anchorView: UIView = innerRankingsView + + var constraints = [ + innerRankingsView.topAnchor.constraint(equalTo: rankingView.topAnchor), + innerRankingsView.bottomAnchor.constraint(equalTo: rankingView.bottomAnchor), + innerRankingsView.centerXAnchor.constraint(equalTo: rankingView.centerXAnchor), + innerRankingsView.widthAnchor.constraint(equalTo: rankingView.widthAnchor) + ] + + var lastDetailLabel: UILabel? + for index in 0.. UIView { + let historyView = UIView() + historyView.translatesAutoresizingMaskIntoConstraints = false + + guard let entries = localPlayer.raceHistory?.entries else { return historyView } + var constraints = [NSLayoutConstraint]() + let entrySpacing = CGFloat(15) + var anchorView: UIView = historyView + + var isLinkHereShown = false + for (index, entry) in entries.enumerated() { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + + let num = index + 1 + let indexString = num.description + ". " + var fullString = indexString + (entry.page.title ?? "") + var linkHereString = "" + + let entryFont = UIFont.systemFont(ofSize: 18, weight: .medium) + let style = NSMutableParagraphStyle() + style.headIndent = 18 + if num >= 100 { + style.headIndent = 34 + } else if num >= 10 { + style.headIndent = 26 + } + + if entry.linkHere { + linkHereString = "*" + fullString += linkHereString + isLinkHereShown = true + } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: entryFont, + .paragraphStyle: style + ] + + let indexFont = UIFont(name: "Menlo-Bold", size: 14)! + + let mutableString = NSMutableAttributedString(string: fullString, + attributes: attributes) + mutableString.addAttribute(.font, + value: indexFont, + range: NSRange(location: 0, + length: indexString.count - 2)) + mutableString.addAttribute(.foregroundColor, + value: UIColor.darkGray, + range: NSRange(location: 0, + length: indexString.count)) + + if entry.linkHere { + let range = NSRange(location: fullString.count - linkHereString.count, + length: linkHereString.count) + mutableString.addAttribute(.foregroundColor, + value: UIColor.darkGray, + range: range) + } + label.attributedText = mutableString + historyView.addSubview(label) + + let anchor = index == 0 ? anchorView.topAnchor : anchorView.bottomAnchor + constraints.append(contentsOf: [ + label.topAnchor.constraint(equalTo: anchor, constant: entrySpacing), + label.leftAnchor.constraint(equalTo: historyView.leftAnchor), + label.rightAnchor.constraint(equalTo: historyView.rightAnchor) + ]) + anchorView = label + } + + if isLinkHereShown { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.font = UIFont.systemFont(ofSize: 14, weight: .medium) + label.textColor = .darkGray + label.text = "* Link Here" + historyView.addSubview(label) + + constraints.append(contentsOf: [ + label.topAnchor.constraint(equalTo: anchorView.bottomAnchor, constant: entrySpacing), + label.leftAnchor.constraint(equalTo: historyView.leftAnchor), + label.rightAnchor.constraint(equalTo: historyView.rightAnchor) + ]) + anchorView = label + } + + constraints.append(historyView.bottomAnchor.constraint(equalTo: anchorView.bottomAnchor)) + NSLayoutConstraint.activate(constraints) + + return historyView + } +} +//swiftlint:enable function_body_length type_body_length cyclomatic_complexity diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer.swift new file mode 100644 index 0000000..28685da --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultRenderer.swift @@ -0,0 +1,145 @@ +// +// ResultRenderer.swift +// WikiRaces +// +// Created by Andrew Finke on 1/28/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit +import WKRKit + +//swiftlint:disable function_body_length +class ResultRenderer { + + // MARK: - Types + + private class RenderView: UIView { + var onLayout: (() -> Void)? + override func layoutSubviews() { + super.layoutSubviews() + onLayout?() + } + } + + // MARK: - Properties + + let tintColor = #colorLiteral(red: 54.0/255.0, green: 54.0/255.0, blue: 54.0/255.0, alpha: 1.0) + private var isRendering = false + + // MARK: - Rendering + + func render(with results: WKRResultsInfo, + for localPlayer: WKRPlayer, + on canvasView: UIView, + completion: @escaping (UIImage) -> Void) { + + guard !isRendering else { return } + isRendering = true + + let view = RenderView() + view.isHidden = true + view.backgroundColor = .white + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = 25 + canvasView.addSubview(view) + + let innerView = UIView() + innerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(innerView) + + let bannerView = createBannerView() + let rankingHeaderView = createHeaderView(title: "rankings") + let rankingView = createRankingView(for: results, localPlayer: localPlayer) + + let historyHeaderView: UIView + if localPlayer.raceHistory?.entries == nil { + historyHeaderView = UIView() + } else { + historyHeaderView = createHeaderView(title: localPlayer.name + "'s path") + } + let historyView = createHistoryView(for: localPlayer) + + innerView.addSubview(bannerView) + innerView.addSubview(rankingHeaderView) + innerView.addSubview(rankingView) + innerView.addSubview(historyHeaderView) + innerView.addSubview(historyView) + + let inset = CGFloat(20) + let constraints = [ + view.widthAnchor.constraint(equalToConstant: 400), + view.topAnchor.constraint(equalTo: canvasView.bottomAnchor), + + innerView.topAnchor.constraint(equalTo: view.topAnchor, constant: inset), + innerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -inset), + innerView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: inset), + innerView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -inset), + + bannerView.topAnchor.constraint(equalTo: innerView.topAnchor), + bannerView.widthAnchor.constraint(equalTo: innerView.widthAnchor), + bannerView.heightAnchor.constraint(equalToConstant: 90), + + rankingHeaderView.topAnchor.constraint(equalTo: bannerView.bottomAnchor), + rankingHeaderView.widthAnchor.constraint(equalTo: innerView.widthAnchor), + + rankingView.topAnchor.constraint(equalTo: rankingHeaderView.bottomAnchor), + rankingView.widthAnchor.constraint(equalTo: innerView.widthAnchor), + + historyHeaderView.topAnchor.constraint(equalTo: rankingView.bottomAnchor), + historyHeaderView.widthAnchor.constraint(equalTo: innerView.widthAnchor), + + historyView.topAnchor.constraint(equalTo: historyHeaderView.bottomAnchor), + historyView.bottomAnchor.constraint(equalTo: innerView.bottomAnchor), + historyView.widthAnchor.constraint(equalTo: innerView.widthAnchor) + ] + + view.onLayout = { [weak self] in + self?.render(view: view, completion: completion) + view.onLayout = nil + view.removeFromSuperview() + } + NSLayoutConstraint.activate(constraints) + } + + private func render(view: UIView, completion: (UIImage) -> Void) { + let width = CGFloat(1020) / UIScreen.main.scale + let ratio = view.bounds.width / width + let size = CGSize(width: width, height: view.bounds.height / ratio) + + let format = UIGraphicsImageRendererFormat() + format.opaque = false + if #available(iOS 12.0, *) { + format.preferredRange = .standard + } else { + format.prefersExtendedRange = false + } + + let fullRenderer = UIGraphicsImageRenderer(size: view.bounds.size, format: format) + view.isHidden = false + let fullImage = fullRenderer.image { ctx in + view.layer.render(in: ctx.cgContext) + } + view.removeFromSuperview() + + let resizedRenderer = UIGraphicsImageRenderer(size: size, format: format) + let image = resizedRenderer.image { _ in + fullImage.draw(in: CGRect(origin: .zero, size: size)) + } + + #if DEBUG + let pngData = resizedRenderer.pngData { _ in + fullImage.draw(in: CGRect(origin: .zero, size: size)) + } + let path = NSTemporaryDirectory().appending(Date().timeIntervalSince1970.description + ".png") + let url = URL(fileURLWithPath: path) + try? pngData.write(to: url) + print(url) + #endif + + isRendering = false + completion(image) + } + +} +//swiftlint:enable function_body_length type_body_length cyclomatic_complexity diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsTableViewCell.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsTableViewCell.swift index 19be928..a1812cb 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsTableViewCell.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsTableViewCell.swift @@ -147,13 +147,16 @@ internal class ResultsTableViewCell: UITableViewCell { // MARK: - Updating - func update(playerName: String, detail: String, subtitle: NSAttributedString, animated: Bool) { + private func update(playerName: NSAttributedString, + detail: String, + subtitle: NSAttributedString, + animated: Bool) { if animated { UIView.transition(with: playerLabel, duration: WKRAnimationDurationConstants.resultsCellLabelsFade, options: .transitionCrossDissolve, animations: { [weak self] in - self?.playerLabel.text = playerName + self?.playerLabel.attributedText = playerName }, completion: nil) UIView.transition(with: detailLabel, duration: WKRAnimationDurationConstants.resultsCellLabelsFade, @@ -168,19 +171,26 @@ internal class ResultsTableViewCell: UITableViewCell { self?.subtitleLabel.attributedText = subtitle }, completion: nil) } else { - playerLabel.text = playerName + playerLabel.attributedText = playerName detailLabel.text = detail subtitleLabel.attributedText = subtitle } } - func update(for player: WKRPlayer, animated: Bool) { + func updateResults(for player: WKRPlayer, animated: Bool) { guard let history = player.raceHistory, let entry = history.entries.last else { - subtitleLabel.text = "Unknown Page" + playerLabel.text = player.name + subtitleLabel.text = "-" + detailLabel.text = "-" + if player.state == .forcedEnd { + detailLabel.text = "DNF" + } else if player.state == .quit { + detailLabel.text = "Quit" + } return } - let pageTitle = entry.page.title ?? "Unknown Page" + let pageTitle = entry.page.title ?? "-" var pageTitleAttributedString = NSMutableAttributedString(string: pageTitle, attributes: nil) if entry.linkHere { let detail = " Link Here" @@ -195,7 +205,7 @@ internal class ResultsTableViewCell: UITableViewCell { } var detailString = player.state.text - if player.state == .foundPage, let duration = DurationFormatter.string(for: history.duration) { + if player.state == .foundPage, let duration = WKRDurationFormatter.string(for: history.duration) { detailString = duration } else if player.state == .racing { detailString = "" @@ -204,11 +214,58 @@ internal class ResultsTableViewCell: UITableViewCell { } isShowingActivityIndicatorView = player.state == .racing - update(playerName: player.name, + update(playerName: playerNameAttributedString(for: player), detail: detailString, subtitle: pageTitleAttributedString, animated: animated) + } + + func updateStandings(for sessionResults: WKRResultsInfo.WKRProfileSessionResults) { + isShowingActivityIndicatorView = false + isShowingCheckmark = false + + let detailString: String + if sessionResults.points == 1 { + detailString = sessionResults.points.description + " PT" + } else { + detailString = sessionResults.points.description + " PTS" + } + var subtitleString: String + if sessionResults.ranking == 1 { + subtitleString = "1st Place" + } else if sessionResults.ranking == 2 { + subtitleString = "2nd Place" + } else if sessionResults.ranking == 3 { + subtitleString = "3rd Place" + } else { + subtitleString = "\(sessionResults.ranking)th Place" + } + subtitleString += sessionResults.isTied ? " (Tied)" : "" + + update(playerName: NSAttributedString(string: sessionResults.profile.name), + detail: detailString, + subtitle: NSAttributedString(string: subtitleString), + animated: false) + } + + // MARK: - Other + + func playerNameAttributedString(for player: WKRPlayer) -> NSAttributedString { + if let isCreator = player.isCreator, isCreator { + let name = player.name + let nameAttributedString = NSMutableAttributedString(string: name, attributes: nil) + let range = NSRange(location: 0, length: name.count) + let attributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: UIColor(displayP3Red: 69.0/255.0, + green: 145.0/255.0, + blue: 208.0/255.0, + alpha: 1.0) + ] + nameAttributedString.addAttributes(attributes, range: range) + return nameAttributedString + } + return NSAttributedString(string: player.name, attributes: nil) } } diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+Actions.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+Actions.swift new file mode 100644 index 0000000..2e12aaf --- /dev/null +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+Actions.swift @@ -0,0 +1,45 @@ +// +// ResultsViewController+Actions.swift +// WikiRaces +// +// Created by Andrew Finke on 1/29/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import UIKit + +extension ResultsViewController { + + // MARK: - Actions + + @objc func doneButtonPressed(_ sender: Any) { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + guard let alertController = quitAlertController else { + PlayerAnonymousMetrics.log(event: .backupQuit, + attributes: ["RawGameState": state.rawValue]) + listenerUpdate?(.quit) + return + } + present(alertController, animated: true, completion: nil) + } + + @objc func addPlayersBarButtonItemPressed() { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + guard let controller = addPlayersViewController else { return } + present(controller, animated: true, completion: nil) + PlayerAnonymousMetrics.log(event: .hostStartMidMatchInviting) + } + + @objc func shareResultsBarButtonItemPressed(_ sender: UIBarButtonItem) { + PlayerAnonymousMetrics.log(event: .userAction(#function)) + guard let image = resultImage else { return } + + let controller = UIActivityViewController(activityItems: [ + image, + "#WikiRaces3" + ], applicationActivities: nil) + controller.popoverPresentationController?.barButtonItem = sender + present(controller, animated: true, completion: nil) + PlayerAnonymousMetrics.log(event: .openedShare) + } +} diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift index 72ef228..00c1246 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+KB.swift @@ -40,7 +40,7 @@ extension ResultsViewController { @objc private func keyboardAttemptQuit(_ keyCommand: UIKeyCommand) { guard presentedViewController == nil, navigationItem.rightBarButtonItem?.isEnabled ?? false else { return } - quitButtonPressed(keyCommand) + doneButtonPressed(keyCommand) } } diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+TableView.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+TableView.swift index 4a04e6d..aa6efd8 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+TableView.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController+TableView.swift @@ -30,37 +30,12 @@ extension ResultsViewController: UITableViewDataSource, UITableViewDelegate { private func configure(cell: ResultsTableViewCell, with resultsInfo: WKRResultsInfo, at index: Int) { switch state { case .results, .hostResults: - let raceResults = resultsInfo.raceResults(at: index) - cell.isShowingCheckmark = readyStates?.playerReady(raceResults.player) ?? false - cell.update(for: raceResults.player, animated: true) + let player = resultsInfo.raceRankingsPlayer(at: index) + cell.isShowingCheckmark = readyStates?.isPlayerReady(player) ?? false + cell.updateResults(for: player, animated: true) case .points: let sessionResults = resultsInfo.sessionResults(at: index) - cell.isShowingActivityIndicatorView = false - cell.isShowingCheckmark = false - - let detailString: String - if sessionResults.points == 1 { - detailString = sessionResults.points.description + " PT" - } else { - detailString = sessionResults.points.description + " PTS" - } - - var subtitleString: String - if sessionResults.ranking == 1 { - subtitleString = "1st Place" - } else if sessionResults.ranking == 2 { - subtitleString = "2nd Place" - } else if sessionResults.ranking == 3 { - subtitleString = "3rd Place" - } else { - subtitleString = "\(sessionResults.ranking)th Place" - } - subtitleString += sessionResults.isTied ? " (Tied)" : "" - - cell.update(playerName: sessionResults.profile.name, - detail: detailString, - subtitle: NSAttributedString(string: subtitleString), - animated: false) + cell.updateStandings(for: sessionResults) default: fatalError("Unexpected state \(state)") } @@ -69,16 +44,22 @@ extension ResultsViewController: UITableViewDataSource, UITableViewDelegate { // MARK: - UITableViewDelegate func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - PlayerMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .userAction(#function)) guard let resultsInfo = resultsInfo else { return } - let raceResults = resultsInfo.raceResults(at: indexPath.row) - performSegue(withIdentifier: "showHistory", sender: raceResults.player) + let controller = HistoryViewController(style: .grouped) + historyViewController = controller + controller.player = resultsInfo.raceRankingsPlayer(at: indexPath.row) + + let navController = UINavigationController(rootViewController: controller) + navController.modalPresentationStyle = .formSheet + present(navController, animated: true, completion: nil) - PlayerMetrics.log(event: .openedHistory, attributes: ["GameState": state.rawValue.description as Any]) + PlayerAnonymousMetrics.log(event: .openedHistory, + attributes: ["GameState": state.rawValue.description as Any]) } } diff --git a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift index 3351bca..befdb93 100644 --- a/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/ResultsViewController/ResultsViewController.swift @@ -12,27 +12,46 @@ import WKRUIKit internal class ResultsViewController: CenteredTableViewController { - // MARK: - Properties + // MARK: - Types + + enum ListenerUpdate { + case readyButtonPressed + case quit + } - private var historyViewController: HistoryViewController? + // MARK: - Properties - var readyButtonPressed: (() -> Void)? + var listenerUpdate: ((ListenerUpdate) -> Void)? + var historyViewController: HistoryViewController? - var backupQuit: (() -> Void)? var quitAlertController: UIAlertController? var addPlayersViewController: UIViewController? + var addPlayersBarButtonItem: UIBarButtonItem? + var shareResultsBarButtonItem: UIBarButtonItem? + var isAnimatingToPointsStandings = false var hasAnimatedToPointsStandings = false + let resultRenderer = ResultRenderer() + var resultImage: UIImage? { + didSet { + updatedResultsImage() + } + } + // MARK: - Game States + var localPlayer: WKRPlayer? + var isPlayerHost = false { didSet { if isPlayerHost && addPlayersViewController != nil { - navigationItem.leftBarButtonItem?.isEnabled = false + addPlayersBarButtonItem?.isEnabled = false + } else if let button = shareResultsBarButtonItem { + navigationItem.leftBarButtonItems = [button] } else { - navigationItem.leftBarButtonItem = nil + navigationItem.leftBarButtonItems = nil } } } @@ -81,45 +100,32 @@ internal class ResultsViewController: CenteredTableViewController { tableView.register(ResultsTableViewCell.self, forCellReuseIdentifier: reuseIdentifier) tableView.estimatedRowHeight = 150 tableView.rowHeight = UITableView.automaticDimension - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return UIStatusBarStyle.wkrStatusBarStyle - } - // MARK: - Actions - - @IBAction func quitButtonPressed(_ sender: Any) { - PlayerMetrics.log(event: .userAction(#function)) - guard let alertController = quitAlertController else { - PlayerMetrics.log(event: .backupQuit, attributes: ["GameState": state.rawValue.description as Any]) - self.backupQuit?() - return + let shareResultsBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, + target: self, + action: #selector(shareResultsBarButtonItemPressed(_:))) + shareResultsBarButtonItem.isEnabled = false + let addPlayersBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, + target: self, + action: #selector(addPlayersBarButtonItemPressed)) + addPlayersBarButtonItem.isEnabled = false + + var items = [shareResultsBarButtonItem] + if isPlayerHost && addPlayersViewController != nil { + items.append(addPlayersBarButtonItem) } - present(alertController, animated: true, completion: nil) - PlayerMetrics.log(presentingOf: alertController, on: self) - } + navigationItem.leftBarButtonItems = items - @IBAction func addPlayersBarButtonItemPressed(_ sender: Any) { - PlayerMetrics.log(event: .userAction(#function)) - guard let controller = addPlayersViewController else { return } - present(controller, animated: true, completion: nil) - PlayerMetrics.log(event: .hostStartMidMatchInviting) - PlayerMetrics.log(presentingOf: controller, on: self) - } - - override func overlayButtonPressed() { - PlayerMetrics.log(event: .userAction(#function)) + self.shareResultsBarButtonItem = shareResultsBarButtonItem + self.addPlayersBarButtonItem = addPlayersBarButtonItem - navigationItem.leftBarButtonItem?.isEnabled = false - readyButtonPressed?() - isOverlayButtonHidden = true - - UIView.animate(withDuration: WKRAnimationDurationConstants.resultsOverlayButtonToggle) { - self.view.layoutIfNeeded() - } + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, + target: self, + action: #selector(doneButtonPressed)) + } - PlayerMetrics.log(event: .pressedReadyButton, attributes: ["Time": timeRemaining as Any]) + override var preferredStatusBarStyle: UIStatusBarStyle { + return UIStatusBarStyle.wkrStatusBarStyle } // MARK: - Game Updates @@ -161,7 +167,7 @@ internal class ResultsViewController: CenteredTableViewController { if isPlayerHost, let results = resultsInfo { DispatchQueue.global().async { - PlayerMetrics.record(results: results) + PlayerDatabaseMetrics.shared.record(results: results) } } } @@ -186,6 +192,26 @@ internal class ResultsViewController: CenteredTableViewController { self.guideLabel.text = "TAP PLAYER TO VIEW HISTORY" self.descriptionLabel.text = "NEXT ROUND STARTS IN " + self.timeRemaining.description + " S" }, completion: nil) + + guard let localPlayer = localPlayer else { return } + var resultsPlayer: WKRPlayer? + + for index in 0..<(resultsInfo?.playerCount ?? 0) { + let player = resultsInfo?.raceRankingsPlayer(at: index) + if localPlayer == player { + resultsPlayer = player + } + } + + guard let window = UIApplication.shared.keyWindow, + let results = self.resultsInfo, + let player = resultsPlayer, + player.raceHistory != nil else { return } + + resultRenderer.render(with: results, for: player, on: window) { [weak self] image in + self?.resultImage = image + self?.shareResultsBarButtonItem?.isEnabled = true + } } else { descriptionLabel.text = "NEXT ROUND STARTS IN " + timeRemaining.description + " S" } @@ -193,6 +219,12 @@ internal class ResultsViewController: CenteredTableViewController { // MARK: - Helpers + private func updatedResultsImage() { + guard let image = resultImage, UserDefaults.standard.bool(forKey: "force_save_result_image") else { return } + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + PlayerAnonymousMetrics.log(event: .automaticResultsImageSave) + } + private func updateTableViewForNewReadyStates() { guard !(state == .points && !hasAnimatedToPointsStandings) else { return @@ -205,12 +237,12 @@ internal class ResultsViewController: CenteredTableViewController { } for index in 0.. IndexPath? { guard let lastIndexPath = tableView.indexPathForSelectedRow else { - UISelectionFeedbackGenerator().selectionChanged() return indexPath } if lastIndexPath == indexPath { @@ -41,13 +40,13 @@ extension VotingViewController: UITableViewDataSource, UITableViewDelegate { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - PlayerMetrics.log(event: .userAction(#function)) + PlayerAnonymousMetrics.log(event: .userAction(#function)) guard let vote = voteInfo?.page(for: indexPath.row) else { return } - playerVoted?(vote.page) + listenerUpdate?(.voted(vote.page)) } } diff --git a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift b/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift index efbe6d7..19603a7 100644 --- a/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift +++ b/WikiRaces/Shared/Race View Controllers/VotingViewController/VotingViewController.swift @@ -12,14 +12,19 @@ import WKRUIKit internal class VotingViewController: CenteredTableViewController { + // MARK: - Types + + enum ListenerUpdate { + case voted(WKRPage) + case quit + } + // MARK: - Properties private var isShowingGuide = false private var isShowingVoteCountdown = true - var playerVoted: ((WKRPage) -> Void)? - - var backupQuit: (() -> Void)? + var listenerUpdate: ((ListenerUpdate) -> Void)? var quitAlertController: UIAlertController? var voteInfo: WKRVoteInfo? { @@ -79,7 +84,12 @@ internal class VotingViewController: CenteredTableViewController { guideLabel.text = "TAP ARTICLE TO VOTE" descriptionLabel.text = "VOTING STARTS SOON" - tableView.register(VotingTableViewCell.self, forCellReuseIdentifier: reuseIdentifier) + tableView.register(VotingTableViewCell.self, + forCellReuseIdentifier: reuseIdentifier) + + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, + target: self, + action: #selector(doneButtonPressed)) } override func viewDidAppear(_ animated: Bool) { @@ -93,15 +103,15 @@ internal class VotingViewController: CenteredTableViewController { // MARK: - Actions - @IBAction func quitButtonPressed(_ sender: Any) { - PlayerMetrics.log(event: .userAction(#function)) + @objc func doneButtonPressed(_ sender: Any) { + PlayerAnonymousMetrics.log(event: .userAction(#function)) guard let alertController = quitAlertController else { - PlayerMetrics.log(event: .backupQuit, attributes: ["GameState": WKRGameState.voting.rawValue.description]) - self.backupQuit?() + PlayerAnonymousMetrics.log(event: .backupQuit, + attributes: ["RawGameState": WKRGameState.voting.rawValue]) + self.listenerUpdate?(.quit) return } present(alertController, animated: true, completion: nil) - PlayerMetrics.log(presentingOf: alertController, on: self) } // MARK: - Helpers diff --git a/WikiRaces/Shared/Resources/Settings.bundle/Root.plist b/WikiRaces/Shared/Resources/Settings.bundle/Root.plist index bf9f931..38f5b7d 100644 --- a/WikiRaces/Shared/Resources/Settings.bundle/Root.plist +++ b/WikiRaces/Shared/Resources/Settings.bundle/Root.plist @@ -6,6 +6,22 @@ Root PreferenceSpecifiers + + Type + PSToggleSwitchSpecifier + Title + Always save result image + Key + force_save_result_image + DefaultValue + + + + Type + PSGroupSpecifier + Title + LOCAL RACES NAME + Type PSTextFieldSpecifier @@ -24,6 +40,12 @@ AutocorrectionType No + + Type + PSGroupSpecifier + Title + GLOBAL RACES USE GAME CENTER NAME + diff --git a/WikiRaces/Shared/Resources/SharedAssets.xcassets/Back.imageset/Back.pdf b/WikiRaces/Shared/Resources/SharedAssets.xcassets/Back.imageset/Back.pdf new file mode 100644 index 0000000..4a825bc Binary files /dev/null and b/WikiRaces/Shared/Resources/SharedAssets.xcassets/Back.imageset/Back.pdf differ diff --git a/WikiRaces/Shared/Resources/SharedAssets.xcassets/Back.imageset/Contents.json b/WikiRaces/Shared/Resources/SharedAssets.xcassets/Back.imageset/Contents.json new file mode 100644 index 0000000..e39a89b --- /dev/null +++ b/WikiRaces/Shared/Resources/SharedAssets.xcassets/Back.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Back.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/WikiRaces/WikiRaces/Assets.xcassets/PreviewIcon.imageset/Contents.json b/WikiRaces/Shared/Resources/SharedAssets.xcassets/PreviewIcon.imageset/Contents.json similarity index 73% rename from WikiRaces/WikiRaces/Assets.xcassets/PreviewIcon.imageset/Contents.json rename to WikiRaces/Shared/Resources/SharedAssets.xcassets/PreviewIcon.imageset/Contents.json index 0abb4aa..3e6b76e 100644 --- a/WikiRaces/WikiRaces/Assets.xcassets/PreviewIcon.imageset/Contents.json +++ b/WikiRaces/Shared/Resources/SharedAssets.xcassets/PreviewIcon.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "iPhone-60@3x Copy 3.png" + "filename" : "PreviewIcon.png" } ], "info" : { diff --git a/WikiRaces/WikiRaces/Assets.xcassets/PreviewIcon.imageset/iPhone-60@3x Copy 3.png b/WikiRaces/Shared/Resources/SharedAssets.xcassets/PreviewIcon.imageset/PreviewIcon.png similarity index 100% rename from WikiRaces/WikiRaces/Assets.xcassets/PreviewIcon.imageset/iPhone-60@3x Copy 3.png rename to WikiRaces/Shared/Resources/SharedAssets.xcassets/PreviewIcon.imageset/PreviewIcon.png diff --git a/WikiRaces/WikiRaces (Multi-Window)/Info.plist b/WikiRaces/WikiRaces (Multi-Window)/Info.plist index 77178d4..0fedbe1 100644 --- a/WikiRaces/WikiRaces (Multi-Window)/Info.plist +++ b/WikiRaces/WikiRaces (Multi-Window)/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 803 + 857 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/WikiRaces/WikiRaces (Multi-Window)/ViewController.swift b/WikiRaces/WikiRaces (Multi-Window)/ViewController.swift index 96c8b48..7143cd5 100644 --- a/WikiRaces/WikiRaces (Multi-Window)/ViewController.swift +++ b/WikiRaces/WikiRaces (Multi-Window)/ViewController.swift @@ -37,41 +37,30 @@ class ViewController: UIViewController { if twoRows { for xPos in 0.. MenuViewController { - let controller = UIStoryboard(name: "Main", bundle: nil) - .instantiateInitialViewController() as! UINavigationController - return controller.viewControllers.first as! MenuViewController + func createDebugWindow(frame: CGRect, named name: String) { + let window = DebugWindow(frame: frame) + window.playerName = name + window.rootViewController = MenuViewController() + window.makeKeyAndVisible() } } diff --git a/WikiRaces/WikiRaces (UI Catalog)/AppDelegate.swift b/WikiRaces/WikiRaces (UI Catalog)/AppDelegate.swift index 99d2692..1be58c7 100644 --- a/WikiRaces/WikiRaces (UI Catalog)/AppDelegate.swift +++ b/WikiRaces/WikiRaces (UI Catalog)/AppDelegate.swift @@ -15,6 +15,8 @@ class AppDelegate: WKRAppDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { configureAppearance() configureConstants() + + cleanTempDirectory() return true } diff --git a/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift b/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift index ada1a7b..a6f0b25 100644 --- a/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift +++ b/WikiRaces/WikiRaces (UI Catalog)/ViewController.swift @@ -13,27 +13,30 @@ import UIKit internal class ViewController: UIViewController { - //swiftlint:disable line_length force_cast + // MARK: - ResultsViewController Testing var players = [WKRPlayer]() + let res = ResultRenderer() + var rendered = false + + //swiftlint:disable:next function_body_length override func viewDidLoad() { super.viewDidLoad() - let nav = viewController() as! UINavigationController - - let controller = nav.rootViewController as! ResultsViewController + let controller = ResultsViewController() + let nav = UINavigationController(rootViewController: controller) (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = nav - let names = ["Andrew", "Carol", "Tom", "Lisa", "Midnight", "Uncle D", "Pops", "Sam"] + //let names = ["Andrew", "Carol", "Tom", "Lisa", "Midnight", "Uncle D", "Pops", "Sam"] + let names = ["Andrew", "Carol", "Tom", "Lisa"] + for var index in 0.. 4 { + player.state = .foundPage +// } else if arc4random() % 25 == 0 { +// player.state = .forcedEnd +// } else if arc4random() % 30 == 0 { +// player.state = .quit +// } else if arc4random() % 30 == 0 { +// player.state = .forfeited + } else { + player.finishedViewingLastPage(pointsScrolled: 5) + player.nowViewing(page: page, linkHere: arc4random() % 5 == 0) + } + + } // controller.player = self.players[0] - controller.resultsInfo = WKRResultsInfo(players: self.players, + controller.resultsInfo = WKRResultsInfo(racePlayers: self.players, racePoints: [:], sessionPoints: [:]) - print("Updating------") + + controller.showReadyUpButton(true) + } } } - let time: CGFloat = CGFloat(arc4random() % 40) / 10.0 - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + let time: DispatchTimeInterval = .seconds(Int.random(in: 1...5)) + DispatchQueue.main.asyncAfter(deadline: .now() + time) { if arc4random() % 3 == 0 { // controller.state = .points random() } else { random() } + + if self.players.filter ({$0.state == .racing }).isEmpty && !self.rendered { + self.rendered = true + for player in self.players { + ResultRenderer().render(with: controller.resultsInfo!, + for: player, + on: controller.contentView, + completion: { _ in + }) + } + } } } DispatchQueue.main.asyncAfter(deadline: .now() + 3) { random() - // playerOneHistory.finishedViewingLastPage() - // playerOneHistory.append(playerOnePageTwo, linkHere: false) - // playerOne.raceHistory = playerOneHistory - // - // playerTwoHistory.finishedViewingLastPage() - // playerTwoHistory.append(playerOnePageOne, linkHere: true) - // playerTwo.raceHistory = playerTwoHistory - // playerTwo.state = .foundPage - // - // controller.resultsInfo = WKRResultsInfo(players: [playerOne, playerTwo], racePoints: [:], sessionPoints: [:]) - - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - } - } } } - func viewController() -> UIViewController { - return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ResultsNav") - } - } diff --git a/WikiRaces/WikiRaces.xcodeproj/project.pbxproj b/WikiRaces/WikiRaces.xcodeproj/project.pbxproj index ab32fb1..f72ad0a 100644 --- a/WikiRaces/WikiRaces.xcodeproj/project.pbxproj +++ b/WikiRaces/WikiRaces.xcodeproj/project.pbxproj @@ -9,25 +9,37 @@ /* Begin PBXBuildFile section */ 140564D52112D4BD001E36AB /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 140564D32112D4BC001E36AB /* Crashlytics.framework */; }; 140564D62112D4BD001E36AB /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 140564D42112D4BC001E36AB /* Fabric.framework */; }; - 1410DB371F4F504F00F5CAD7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF8501F362B83000A5D96 /* Main.storyboard */; }; 1410DB3B1F4F510900F5CAD7 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */; }; 1410DB3D1F4F510900F5CAD7 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */; }; + 1414280321FC18F600C48788 /* GameKitConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */; }; + 1414280721FC394600C48788 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* ConnectViewController.swift */; }; + 1414280B21FC437000C48788 /* GameKitConnectViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */; }; + 141598DF2201493E00DA955E /* FirebaseRemoteConfig.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 141598DC2201493D00DA955E /* FirebaseRemoteConfig.framework */; }; + 141598E12201495F00DA955E /* FirebaseABTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 141598E02201495F00DA955E /* FirebaseABTesting.framework */; }; 141892F41F60EABC006748F0 /* MPCConnectViewController+Invite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */; }; - 141892F61F60ECD2006748F0 /* MenuViewController+Segue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F51F60ECD2006748F0 /* MenuViewController+Segue.swift */; }; - 141892F71F60ECD2006748F0 /* MenuViewController+Segue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F51F60ECD2006748F0 /* MenuViewController+Segue.swift */; }; + 141E4CE72200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; + 141E4CE82200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; + 141E4CE92200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */; }; + 141E4CF122012B2F000A0A15 /* PlayerDatabaseMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */; }; 142A09BE210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */; }; 142A09C0210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */; }; 142F714F210C33FC00C66558 /* MenuViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */; }; 142F7153210C34A300C66558 /* MPCHostViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7152210C34A300C66558 /* MPCHostViewController+KB.swift */; }; 142F7155210C35BC00C66558 /* VotingViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */; }; 142F7157210C375F00C66558 /* GameViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142F7156210C375F00C66558 /* GameViewController+KB.swift */; }; + 1437C51F22285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; + 1437C52022285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; + 1437C52122285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */; }; 143948BD2144CC0F00992850 /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; 143948C12144CC8C00992850 /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; - 143A8BBC1F58746800580AA2 /* StatsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* StatsHelper.swift */; }; - 143BB7151F60AEC900D00541 /* StatsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* StatsHelper.swift */; }; - 143BB7181F60BDC000D00541 /* MenuViewController+Leaderboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+Leaderboards.swift */; }; - 143BB7191F60BDC000D00541 /* MenuViewController+Leaderboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+Leaderboards.swift */; }; + 143A8BBC1F58746800580AA2 /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; + 143BB7151F60AEC900D00541 /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; + 143BB7181F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; + 143BB7191F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; 143BB7351F60DF4A00D00541 /* MPCConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7341F60DF4A00D00541 /* MPCConnectViewController.swift */; }; + 14495D8821FF9A0500CAA129 /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; + 14495D8921FF9A0500CAA129 /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; + 14495D8A21FF9A0500CAA129 /* ResultRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */; }; 144A1029202FC79B003DB51A /* CenteredTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */; }; 144A102A202FC79B003DB51A /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1028202FC79B003DB51A /* HelpViewController.swift */; }; 144A102B202FC7A1003DB51A /* CenteredTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */; }; @@ -36,27 +48,42 @@ 144A102E202FC7A2003DB51A /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A1028202FC79B003DB51A /* HelpViewController.swift */; }; 144A5AEC1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */; }; 144A5AED1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */; }; + 14584BC2220B6BE700D63428 /* WKRKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; }; + 14584BC3220B6BE700D63428 /* WKRKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 14584BC7220B6BEE00D63428 /* WKRUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; }; + 14584BC8220B6BEE00D63428 /* WKRUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 145925DE210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */; }; 145925DF210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */; }; 145925E0210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */; }; 1473E2A0210DAD7C00726377 /* HelpViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1473E29F210DAD7C00726377 /* HelpViewController+KB.swift */; }; 147593D11F609911005DFC90 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147593D01F609911005DFC90 /* SnapshotHelper.swift */; }; + 1478B1B122095360009F2F3F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; + 1478B1B222095360009F2F3F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; + 1478B1B322095360009F2F3F /* ResultRenderer+Creation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */; }; + 147EF6F92202436600583D73 /* MPCHostContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147EF6F82202436600583D73 /* MPCHostContext.swift */; }; + 147EF6FA2202436600583D73 /* MPCHostContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147EF6F82202436600583D73 /* MPCHostContext.swift */; }; + 147EF7122202D76000583D73 /* MPCHostContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147EF6F82202436600583D73 /* MPCHostContext.swift */; }; + 1480F6C321FB77D300081F58 /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; + 1480F6C621FB77DE00081F58 /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; 148487F8214F0F3B0098CBFA /* GoogleToolboxForMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487EC214F0F3A0098CBFA /* GoogleToolboxForMac.framework */; }; 148487F9214F0F3B0098CBFA /* GoogleUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487ED214F0F3A0098CBFA /* GoogleUtilities.framework */; }; 148487FA214F0F3B0098CBFA /* Protobuf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487EE214F0F3A0098CBFA /* Protobuf.framework */; }; 148487FB214F0F3B0098CBFA /* FirebaseCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487EF214F0F3B0098CBFA /* FirebaseCore.framework */; }; 148487FC214F0F3B0098CBFA /* nanopb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F0214F0F3B0098CBFA /* nanopb.framework */; }; - 148487FD214F0F3B0098CBFA /* FirebaseCrash.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F1214F0F3B0098CBFA /* FirebaseCrash.framework */; }; 148487FE214F0F3B0098CBFA /* FirebaseInstanceID.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F2214F0F3B0098CBFA /* FirebaseInstanceID.framework */; }; 148487FF214F0F3B0098CBFA /* GoogleAppMeasurement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F3214F0F3B0098CBFA /* GoogleAppMeasurement.framework */; }; 14848800214F0F3B0098CBFA /* FirebasePerformance.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F4214F0F3B0098CBFA /* FirebasePerformance.framework */; }; 14848801214F0F3B0098CBFA /* GTMSessionFetcher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F5214F0F3B0098CBFA /* GTMSessionFetcher.framework */; }; 14848802214F0F3B0098CBFA /* FirebaseCoreDiagnostics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F6214F0F3B0098CBFA /* FirebaseCoreDiagnostics.framework */; }; 14848803214F0F3B0098CBFA /* FirebaseAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148487F7214F0F3B0098CBFA /* FirebaseAnalytics.framework */; }; + 1485B6772230724A00D6800B /* MedalScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B6762230724A00D6800B /* MedalScene.swift */; }; + 1485B6782230724A00D6800B /* MedalScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B6762230724A00D6800B /* MedalScene.swift */; }; + 1485B6792230724A00D6800B /* MedalScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B6762230724A00D6800B /* MedalScene.swift */; }; + 1485B67D223072AB00D6800B /* MedalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B67C223072AB00D6800B /* MedalView.swift */; }; + 1485B67E223072AB00D6800B /* MedalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B67C223072AB00D6800B /* MedalView.swift */; }; + 1485B67F223072AB00D6800B /* MedalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1485B67C223072AB00D6800B /* MedalView.swift */; }; 14891AA6214F6BDB001BDEB8 /* DebugInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */; }; 14891AA9214F6BDE001BDEB8 /* DebugInfoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */; }; - 148B4B2E1F57519F007B70B5 /* GameViewController+Segue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148B4B2D1F57519F007B70B5 /* GameViewController+Segue.swift */; }; - 148B4B2F1F57519F007B70B5 /* GameViewController+Segue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148B4B2D1F57519F007B70B5 /* GameViewController+Segue.swift */; }; 149357D9210E801A00F6453A /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; 149357DA210E801A00F6453A /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; 149357DB210E801A00F6453A /* HistoryViewController+KB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */; }; @@ -65,14 +92,7 @@ 149357E3210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */; }; 149357E4210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */; }; 149357E5210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */; }; - 1497A8551FF476BC0013E9E3 /* StateLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1497A8541FF476BC0013E9E3 /* StateLogViewController.swift */; }; - 1497A8561FF476BC0013E9E3 /* StateLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1497A8541FF476BC0013E9E3 /* StateLogViewController.swift */; }; - 1497A8571FF476BC0013E9E3 /* StateLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1497A8541FF476BC0013E9E3 /* StateLogViewController.swift */; }; - 1497A8591FF478920013E9E3 /* StateLogTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1497A8581FF478920013E9E3 /* StateLogTableViewController.swift */; }; - 1497A85A1FF478920013E9E3 /* StateLogTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1497A8581FF478920013E9E3 /* StateLogTableViewController.swift */; }; - 1497A85B1FF478920013E9E3 /* StateLogTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1497A8581FF478920013E9E3 /* StateLogTableViewController.swift */; }; 149FF84D1F362B83000A5D96 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149FF84C1F362B83000A5D96 /* AppDelegate.swift */; }; - 149FF8521F362B83000A5D96 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF8501F362B83000A5D96 /* Main.storyboard */; }; 149FF8541F362B83000A5D96 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 149FF8531F362B83000A5D96 /* Assets.xcassets */; }; 149FF8571F362B83000A5D96 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF8551F362B83000A5D96 /* LaunchScreen.storyboard */; }; 149FF87B1F362BE4000A5D96 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149FF87A1F362BE4000A5D96 /* AppDelegate.swift */; }; @@ -83,7 +103,35 @@ 149FF8911F362BF1000A5D96 /* WikiRacesScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149FF8901F362BF1000A5D96 /* WikiRacesScreenshots.swift */; }; 14A7C9B31F65A9EB00980E4D /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 143BB7321F60DE9300D00541 /* Settings.bundle */; }; 14A89F681F7ABB2400C85387 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E1B1A01F798A520082F4FA /* CloudKit.framework */; }; + 14B2DD3B22212298009B8AB3 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; + 14B2DD3C22212298009B8AB3 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; + 14B2DD3D22212298009B8AB3 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD3A22212298009B8AB3 /* MenuView.swift */; }; + 14B2DD412221273E009B8AB3 /* MenuView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */; }; + 14B2DD422221273E009B8AB3 /* MenuView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */; }; + 14B2DD432221273E009B8AB3 /* MenuView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */; }; + 14B2DD4522212B96009B8AB3 /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; + 14B2DD4622212B96009B8AB3 /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; + 14B2DD4722212B96009B8AB3 /* MenuView+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */; }; + 14B2DD4C22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */; }; + 14B2DD4D22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */; }; + 14B2DD4E22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */; }; + 14B4DB612224809F007D4B54 /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; + 14B4DB622224809F007D4B54 /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; + 14B4DB632224809F007D4B54 /* MovingPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */; }; + 14B4DB662224F1B9007D4B54 /* GameKitConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */; }; + 14B4DB672224F1BA007D4B54 /* GameKitConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */; }; + 14B4DB682224F1BC007D4B54 /* GameKitConnectViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */; }; + 14B4DB692224F1BD007D4B54 /* GameKitConnectViewController+Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */; }; + 14B4DB6B2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */; }; + 14B4DB6C2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */; }; + 14B4DB6D2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */; }; 14B55C991F3A49D20090E092 /* DebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B55C981F3A49D20090E092 /* DebugWindow.swift */; }; + 14B8F80D222C456B006C7A06 /* GameKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B8F80C222C456B006C7A06 /* GameKit.framework */; }; + 14B8F811222C47FA006C7A06 /* GKMessageImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 14B8F810222C47FA006C7A06 /* GKMessageImage.png */; }; + 14BA538921FE3B1400A8CB01 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* ConnectViewController.swift */; }; + 14BA538D21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; + 14BA538E21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; + 14BA538F21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */; }; 14C25FE01F6F025A00CD7373 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C25FDF1F6F025A00CD7373 /* AppDelegate.swift */; }; 14C25FE21F6F025A00CD7373 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C25FE11F6F025A00CD7373 /* ViewController.swift */; }; 14C25FE51F6F025A00CD7373 /* Main-Catalog.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 14C25FE31F6F025A00CD7373 /* Main-Catalog.storyboard */; }; @@ -91,12 +139,9 @@ 14C25FEA1F6F025A00CD7373 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 14C25FE81F6F025A00CD7373 /* LaunchScreen.storyboard */; }; 14C25FF11F6F028100CD7373 /* MenuTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB381F3689A90086B77F /* MenuTile.swift */; }; 14C25FF21F6F028100CD7373 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; - 14C25FF31F6F028100CD7373 /* MenuViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB401F3689B50086B77F /* MenuViewController+UI.swift */; }; - 14C25FF41F6F028100CD7373 /* MenuViewController+Segue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F51F60ECD2006748F0 /* MenuViewController+Segue.swift */; }; - 14C25FF51F6F028100CD7373 /* MenuViewController+Leaderboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+Leaderboards.swift */; }; + 14C25FF51F6F028100CD7373 /* MenuViewController+GameKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */; }; 14C25FFB1F6F02A500CD7373 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB5C1F368A0C0086B77F /* GameViewController.swift */; }; 14C25FFC1F6F02A500CD7373 /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; - 14C25FFD1F6F02A500CD7373 /* GameViewController+Segue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148B4B2D1F57519F007B70B5 /* GameViewController+Segue.swift */; }; 14C25FFE1F6F02A500CD7373 /* GameViewController+Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */; }; 14C25FFF1F6F02A500CD7373 /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; 14C260001F6F02A500CD7373 /* ResultsViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB301F36899B0086B77F /* ResultsViewController+TableView.swift */; }; @@ -106,11 +151,9 @@ 14C260041F6F02A500CD7373 /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; 14C260051F6F02A500CD7373 /* VotingViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */; }; 14C260061F6F02A500CD7373 /* VotingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */; }; - 14C260071F6F02A500CD7373 /* StatsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* StatsHelper.swift */; }; - 14C260081F6F02A500CD7373 /* DurationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB7C1F368A5A0086B77F /* DurationFormatter.swift */; }; + 14C260071F6F02A500CD7373 /* PlayerStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */; }; 14C260091F6F02A500CD7373 /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB801F368A630086B77F /* CommonExtensions.swift */; }; 14C2600A1F6F02A500CD7373 /* WKRAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */; }; - 14C2600B1F6F032200CD7373 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF8501F362B83000A5D96 /* Main.storyboard */; }; 14C2600D1F6F04A200CD7373 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */; }; 14C4AB2D1F3689900086B77F /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; 14C4AB2F1F3689900086B77F /* ResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */; }; @@ -122,8 +165,6 @@ 14C4AB3B1F3689A90086B77F /* MenuTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB381F3689A90086B77F /* MenuTile.swift */; }; 14C4AB3D1F3689AE0086B77F /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; 14C4AB3F1F3689AE0086B77F /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */; }; - 14C4AB411F3689B50086B77F /* MenuViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB401F3689B50086B77F /* MenuViewController+UI.swift */; }; - 14C4AB431F3689B50086B77F /* MenuViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB401F3689B50086B77F /* MenuViewController+UI.swift */; }; 14C4AB491F3689C60086B77F /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; 14C4AB4B1F3689C60086B77F /* VotingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB481F3689C60086B77F /* VotingViewController.swift */; }; 14C4AB511F3689E20086B77F /* VotingViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */; }; @@ -138,8 +179,6 @@ 14C4AB631F368A170086B77F /* GameViewController+Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */; }; 14C4AB651F368A210086B77F /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; 14C4AB671F368A210086B77F /* GameViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB641F368A210086B77F /* GameViewController+UI.swift */; }; - 14C4AB7D1F368A5A0086B77F /* DurationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB7C1F368A5A0086B77F /* DurationFormatter.swift */; }; - 14C4AB7F1F368A5A0086B77F /* DurationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB7C1F368A5A0086B77F /* DurationFormatter.swift */; }; 14C4AB811F368A630086B77F /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB801F368A630086B77F /* CommonExtensions.swift */; }; 14C4AB831F368A630086B77F /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB801F368A630086B77F /* CommonExtensions.swift */; }; 14C4AB851F368AED0086B77F /* VotingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */; }; @@ -148,7 +187,7 @@ 14C6B1F51FF2EABD00F6B422 /* MPCHostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */; }; 14C6B1F61FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */; }; 14C6B1F71FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */; }; - 14D8AD471F81828D00914E5A /* PlayerMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerMetrics.swift */; }; + 14D8AD471F81828D00914E5A /* PlayerAnonymousMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */; }; 14D8AD481F81835700914E5A /* DebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B55C981F3A49D20090E092 /* DebugWindow.swift */; }; 14DC66D11F90700A0026C6ED /* fabric.apikey in Resources */ = {isa = PBXBuildFile; fileRef = 143BB72F1F60D84100D00541 /* fabric.apikey */; }; 14DC66D21F90700D0026C6ED /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 143054BD1F89581100C0BC27 /* GoogleService-Info.plist */; }; @@ -156,6 +195,9 @@ 14DC66D41F9072180026C6ED /* WKRKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14DC66D71F9072200026C6ED /* WKRUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; }; 14DC66D81F9072200026C6ED /* WKRUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D5521F86A1B0005EB3B9 /* WKRUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 14DD97252202293900AAB389 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1414280621FC394600C48788 /* ConnectViewController.swift */; }; + 14DD97282202294D00AAB389 /* PlayerDatabaseMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */; }; + 14DD97292202295100AAB389 /* PlayerDatabaseMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */; }; 14DF31A8211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; 14DF31A9211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; 14DF31AA211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */; }; @@ -170,8 +212,12 @@ 14DF31BB21169372005BA432 /* MPCConnectViewController+Invite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */; }; 14DFBD1E210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */; }; 14DFBD20210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */; }; - 14E1B19A1F7981C70082F4FA /* PlayerMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerMetrics.swift */; }; - 14E1B19B1F7981C70082F4FA /* PlayerMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerMetrics.swift */; }; + 14E0F19F22303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */; }; + 14E0F1A022303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */; }; + 14E0F1A122303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */; }; + 14E0F1AB22303FD100BFF1E9 /* WikiRacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E0F1AA22303FD100BFF1E9 /* WikiRacesTests.swift */; }; + 14E1B19A1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */; }; + 14E1B19B1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */; }; 14E1B1A11F798A520082F4FA /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E1B1A01F798A520082F4FA /* CloudKit.framework */; }; 14E6D55D1F86A1CA005EB3B9 /* WKRKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; }; 14E6D55E1F86A1CA005EB3B9 /* WKRKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 14E6D55A1F86A1B0005EB3B9 /* WKRKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -180,12 +226,19 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 142B7A4B1F771278004A8AA1 /* PBXContainerItemProxy */ = { + 14584BC4220B6BE700D63428 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 149FF8411F362B83000A5D96 /* Project object */; + containerPortal = 146B99F51F4F4B5600507B3F /* WKRKit.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 149FF8071F362B3D000A5D96; + remoteInfo = WKRKit; + }; + 14584BC9220B6BEE00D63428 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146B9A061F4F4B6100507B3F /* WKRUIKit.xcodeproj */; proxyType = 1; - remoteGlobalIDString = 14C25FDC1F6F025A00CD7373; - remoteInfo = "WikiRaces (UI Catalog)"; + remoteGlobalIDString = 149FF7F21F362B0D000A5D96; + remoteInfo = WKRUIKit; }; 149FF8931F362BF1000A5D96 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -222,6 +275,13 @@ remoteGlobalIDString = 149FF7F21F362B0D000A5D96; remoteInfo = WKRUIKit; }; + 14E0F1AD22303FD100BFF1E9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 149FF8411F362B83000A5D96 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 149FF8481F362B83000A5D96; + remoteInfo = WikiRaces; + }; 14E6D5511F86A1B0005EB3B9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 146B9A061F4F4B6100507B3F /* WKRUIKit.xcodeproj */; @@ -297,6 +357,8 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 14584BC8220B6BEE00D63428 /* WKRUIKit.framework in Embed Frameworks */, + 14584BC3220B6BE700D63428 /* WKRKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -307,26 +369,32 @@ 140564D32112D4BC001E36AB /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Crashlytics.framework; sourceTree = ""; }; 140564D42112D4BC001E36AB /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Fabric.framework; sourceTree = ""; }; 1410DB3A1F4F510900F5CAD7 /* SharedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SharedAssets.xcassets; sourceTree = ""; }; + 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameKitConnectViewController.swift; sourceTree = ""; }; + 1414280621FC394600C48788 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = ""; }; + 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameKitConnectViewController+Match.swift"; sourceTree = ""; }; + 141598DC2201493D00DA955E /* FirebaseRemoteConfig.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseRemoteConfig.framework; sourceTree = ""; }; + 141598E02201495F00DA955E /* FirebaseABTesting.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseABTesting.framework; sourceTree = ""; }; 141892F31F60EABC006748F0 /* MPCConnectViewController+Invite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCConnectViewController+Invite.swift"; sourceTree = ""; }; - 141892F51F60ECD2006748F0 /* MenuViewController+Segue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+Segue.swift"; sourceTree = ""; }; + 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsViewController+Actions.swift"; sourceTree = ""; }; + 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDatabaseMetrics.swift; sourceTree = ""; }; 142A09BD210EEBD000979C46 /* MPCConnectViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCConnectViewController+KB.swift"; sourceTree = ""; }; - 142B7A461F771278004A8AA1 /* WikiRacesCatalogScreenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WikiRacesCatalogScreenshots.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+KB.swift"; sourceTree = ""; }; 142F7152210C34A300C66558 /* MPCHostViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCHostViewController+KB.swift"; sourceTree = ""; }; 142F7154210C35BC00C66558 /* VotingViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VotingViewController+KB.swift"; sourceTree = ""; }; 142F7156210C375F00C66558 /* GameViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+KB.swift"; sourceTree = ""; }; 143054BD1F89581100C0BC27 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 143054CF1F8959A900C0BC27 /* Firebase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Firebase.h; sourceTree = ""; }; 143054D01F8959DA00C0BC27 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; + 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTableViewStatsCell.swift; sourceTree = ""; }; 143948BC2144CC0F00992850 /* DebugInfoTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInfoTableViewController.swift; sourceTree = ""; }; 143948C02144CC8C00992850 /* DebugInfoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInfoTableViewCell.swift; sourceTree = ""; }; - 143A8BBB1F58746800580AA2 /* StatsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsHelper.swift; sourceTree = ""; }; - 143BB7171F60BDC000D00541 /* MenuViewController+Leaderboards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+Leaderboards.swift"; sourceTree = ""; }; + 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatsManager.swift; sourceTree = ""; }; + 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+GameKit.swift"; sourceTree = ""; }; 143BB72E1F60D84100D00541 /* fabric.buildsecret */ = {isa = PBXFileReference; lastKnownFileType = text; path = fabric.buildsecret; sourceTree = SOURCE_ROOT; }; 143BB72F1F60D84100D00541 /* fabric.apikey */ = {isa = PBXFileReference; lastKnownFileType = text; path = fabric.apikey; sourceTree = SOURCE_ROOT; }; 143BB7321F60DE9300D00541 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 143BB7341F60DF4A00D00541 /* MPCConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCConnectViewController.swift; sourceTree = ""; }; 144280EC1F5883AB002D977F /* WikiRaces.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WikiRaces.entitlements; sourceTree = ""; }; + 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultRenderer.swift; sourceTree = ""; }; 144A1027202FC79B003DB51A /* CenteredTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CenteredTableViewController.swift; sourceTree = ""; }; 144A1028202FC79B003DB51A /* HelpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewController.swift; sourceTree = ""; }; 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRAppDelegate.swift; sourceTree = ""; }; @@ -338,27 +406,26 @@ 147593CE1F609902005DFC90 /* Fastfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Fastfile; sourceTree = SOURCE_ROOT; }; 147593CF1F609902005DFC90 /* Snapfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Snapfile; sourceTree = SOURCE_ROOT; }; 147593D01F609911005DFC90 /* SnapshotHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; }; + 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultRenderer+Creation.swift"; sourceTree = ""; }; + 147EF6F82202436600583D73 /* MPCHostContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MPCHostContext.swift; path = "Shared/Menu View Controllers/Connect View Controllers/Multipeer Connectivity/MPCHostContext.swift"; sourceTree = SOURCE_ROOT; }; 148487EC214F0F3A0098CBFA /* GoogleToolboxForMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GoogleToolboxForMac.framework; sourceTree = ""; }; 148487ED214F0F3A0098CBFA /* GoogleUtilities.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GoogleUtilities.framework; sourceTree = ""; }; 148487EE214F0F3A0098CBFA /* Protobuf.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Protobuf.framework; sourceTree = ""; }; 148487EF214F0F3B0098CBFA /* FirebaseCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseCore.framework; sourceTree = ""; }; 148487F0214F0F3B0098CBFA /* nanopb.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = nanopb.framework; sourceTree = ""; }; - 148487F1214F0F3B0098CBFA /* FirebaseCrash.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseCrash.framework; sourceTree = ""; }; 148487F2214F0F3B0098CBFA /* FirebaseInstanceID.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseInstanceID.framework; sourceTree = ""; }; 148487F3214F0F3B0098CBFA /* GoogleAppMeasurement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GoogleAppMeasurement.framework; sourceTree = ""; }; 148487F4214F0F3B0098CBFA /* FirebasePerformance.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebasePerformance.framework; sourceTree = ""; }; 148487F5214F0F3B0098CBFA /* GTMSessionFetcher.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GTMSessionFetcher.framework; sourceTree = ""; }; 148487F6214F0F3B0098CBFA /* FirebaseCoreDiagnostics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseCoreDiagnostics.framework; sourceTree = ""; }; 148487F7214F0F3B0098CBFA /* FirebaseAnalytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = FirebaseAnalytics.framework; sourceTree = ""; }; - 148B4B2D1F57519F007B70B5 /* GameViewController+Segue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+Segue.swift"; sourceTree = ""; }; + 1485B6762230724A00D6800B /* MedalScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedalScene.swift; sourceTree = ""; }; + 1485B67C223072AB00D6800B /* MedalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedalView.swift; sourceTree = ""; }; 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryViewController+KB.swift"; sourceTree = ""; }; 149357DE210E934300F6453A /* MPCConnectViewController+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCConnectViewController+UI.swift"; sourceTree = ""; }; 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCHostPeerStateCell.swift; sourceTree = ""; }; - 1497A8541FF476BC0013E9E3 /* StateLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateLogViewController.swift; sourceTree = ""; }; - 1497A8581FF478920013E9E3 /* StateLogTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateLogTableViewController.swift; sourceTree = ""; }; 149FF8491F362B83000A5D96 /* WikiRaces.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WikiRaces.app; sourceTree = BUILT_PRODUCTS_DIR; }; 149FF84C1F362B83000A5D96 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 149FF8511F362B83000A5D96 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 149FF8531F362B83000A5D96 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 149FF8561F362B83000A5D96 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 149FF8581F362B83000A5D96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -372,9 +439,18 @@ 149FF88E1F362BF1000A5D96 /* WikiRacesScreenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WikiRacesScreenshots.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 149FF8901F362BF1000A5D96 /* WikiRacesScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiRacesScreenshots.swift; sourceTree = ""; }; 149FF8921F362BF1000A5D96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 14A778281F61A9A100823DE8 /* Plans.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = Plans.txt; sourceTree = ""; }; 14A89F671F7ABB2100C85387 /* WikiRaces (Multi-Window).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "WikiRaces (Multi-Window).entitlements"; sourceTree = ""; }; + 14B2DD3A22212298009B8AB3 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; + 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuView+Actions.swift"; sourceTree = ""; }; + 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuView+Setup.swift"; sourceTree = ""; }; + 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalRacesHelper.swift; sourceTree = ""; }; + 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingPuzzleView.swift; sourceTree = ""; }; + 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCHostSoloCell.swift; sourceTree = ""; }; 14B55C981F3A49D20090E092 /* DebugWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugWindow.swift; sourceTree = ""; }; + 14B8F80C222C456B006C7A06 /* GameKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameKit.framework; path = System/Library/Frameworks/GameKit.framework; sourceTree = SDKROOT; }; + 14B8F810222C47FA006C7A06 /* GKMessageImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = GKMessageImage.png; sourceTree = ""; }; + 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+Debug.swift"; sourceTree = ""; }; + 14BAB5332229DC6200C5AE27 /* Firebase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Firebase.h; sourceTree = ""; }; 14C25FDD1F6F025A00CD7373 /* WikiRaces (UI Catalog).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WikiRaces (UI Catalog).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 14C25FDF1F6F025A00CD7373 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 14C25FE11F6F025A00CD7373 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -387,7 +463,6 @@ 14C4AB341F3689A40086B77F /* ResultsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsTableViewCell.swift; sourceTree = ""; }; 14C4AB381F3689A90086B77F /* MenuTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuTile.swift; sourceTree = ""; }; 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; - 14C4AB401F3689B50086B77F /* MenuViewController+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MenuViewController+UI.swift"; sourceTree = ""; }; 14C4AB481F3689C60086B77F /* VotingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingViewController.swift; sourceTree = ""; }; 14C4AB501F3689E20086B77F /* VotingViewController+TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VotingViewController+TableView.swift"; sourceTree = ""; }; 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTableViewCell.swift; sourceTree = ""; }; @@ -395,25 +470,21 @@ 14C4AB5C1F368A0C0086B77F /* GameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; }; 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+Manager.swift"; sourceTree = ""; }; 14C4AB641F368A210086B77F /* GameViewController+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+UI.swift"; sourceTree = ""; }; - 14C4AB7C1F368A5A0086B77F /* DurationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationFormatter.swift; sourceTree = ""; }; 14C4AB801F368A630086B77F /* CommonExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonExtensions.swift; sourceTree = ""; }; 14C4AB841F368AED0086B77F /* VotingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingTableViewCell.swift; sourceTree = ""; }; 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MPCHostViewController.swift; sourceTree = ""; }; 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MPCHostSearchingCell.swift; sourceTree = ""; }; 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKRAnimationDurationConstants.swift; sourceTree = ""; }; 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPCHostViewController+Table.swift"; sourceTree = ""; }; - 14E1B1991F7981C70082F4FA /* PlayerMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerMetrics.swift; sourceTree = ""; }; + 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDatabaseStat.swift; sourceTree = ""; }; + 14E0F1A822303FD100BFF1E9 /* WikiRacesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WikiRacesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 14E0F1AA22303FD100BFF1E9 /* WikiRacesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiRacesTests.swift; sourceTree = ""; }; + 14E0F1AC22303FD100BFF1E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerAnonymousMetrics.swift; sourceTree = ""; }; 14E1B1A01F798A520082F4FA /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 142B7A431F771278004A8AA1 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 149FF8461F362B83000A5D96 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -421,6 +492,7 @@ 14E6D55D1F86A1CA005EB3B9 /* WKRKit.framework in Frameworks */, 14E1B1A11F798A520082F4FA /* CloudKit.framework in Frameworks */, 148487FC214F0F3B0098CBFA /* nanopb.framework in Frameworks */, + 141598E12201495F00DA955E /* FirebaseABTesting.framework in Frameworks */, 148487FB214F0F3B0098CBFA /* FirebaseCore.framework in Frameworks */, 14848801214F0F3B0098CBFA /* GTMSessionFetcher.framework in Frameworks */, 148487FF214F0F3B0098CBFA /* GoogleAppMeasurement.framework in Frameworks */, @@ -430,10 +502,11 @@ 148487F8214F0F3B0098CBFA /* GoogleToolboxForMac.framework in Frameworks */, 14E6D5611F86A1CF005EB3B9 /* WKRUIKit.framework in Frameworks */, 148487FE214F0F3B0098CBFA /* FirebaseInstanceID.framework in Frameworks */, + 14B8F80D222C456B006C7A06 /* GameKit.framework in Frameworks */, 140564D62112D4BD001E36AB /* Fabric.framework in Frameworks */, 148487F9214F0F3B0098CBFA /* GoogleUtilities.framework in Frameworks */, 14848802214F0F3B0098CBFA /* FirebaseCoreDiagnostics.framework in Frameworks */, - 148487FD214F0F3B0098CBFA /* FirebaseCrash.framework in Frameworks */, + 141598DF2201493E00DA955E /* FirebaseRemoteConfig.framework in Frameworks */, 148487FA214F0F3B0098CBFA /* Protobuf.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -456,6 +529,15 @@ runOnlyForDeploymentPostprocessing = 0; }; 14C25FDA1F6F025A00CD7373 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 14584BC7220B6BEE00D63428 /* WKRUIKit.framework in Frameworks */, + 14584BC2220B6BE700D63428 /* WKRKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 14E0F1A522303FD100BFF1E9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -465,19 +547,39 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1414280821FC3C9800C48788 /* GameKit */ = { + isa = PBXGroup; + children = ( + 1414280221FC18F600C48788 /* GameKitConnectViewController.swift */, + 1414280A21FC437000C48788 /* GameKitConnectViewController+Match.swift */, + ); + path = GameKit; + sourceTree = ""; + }; + 1414280921FC3CA000C48788 /* Multipeer Connectivity */ = { + isa = PBXGroup; + children = ( + 147EF6F82202436600583D73 /* MPCHostContext.swift */, + 14DFBD21210EFE8E00BD8DAF /* MPCConnectViewController */, + 14DFBD22210EFE9900BD8DAF /* MPCHostViewController */, + ); + path = "Multipeer Connectivity"; + sourceTree = ""; + }; 143054C01F8958DC00C0BC27 /* Analytics */ = { isa = PBXGroup; children = ( + 14BAB5332229DC6200C5AE27 /* Firebase.h */, 143054D01F8959DA00C0BC27 /* BridgingHeader.h */, - 143054CF1F8959A900C0BC27 /* Firebase.h */, 140564D32112D4BC001E36AB /* Crashlytics.framework */, 140564D42112D4BC001E36AB /* Fabric.framework */, + 141598E02201495F00DA955E /* FirebaseABTesting.framework */, 148487F7214F0F3B0098CBFA /* FirebaseAnalytics.framework */, 148487EF214F0F3B0098CBFA /* FirebaseCore.framework */, 148487F6214F0F3B0098CBFA /* FirebaseCoreDiagnostics.framework */, - 148487F1214F0F3B0098CBFA /* FirebaseCrash.framework */, 148487F2214F0F3B0098CBFA /* FirebaseInstanceID.framework */, 148487F4214F0F3B0098CBFA /* FirebasePerformance.framework */, + 141598DC2201493D00DA955E /* FirebaseRemoteConfig.framework */, 148487F3214F0F3B0098CBFA /* GoogleAppMeasurement.framework */, 148487EC214F0F3A0098CBFA /* GoogleToolboxForMac.framework */, 148487ED214F0F3A0098CBFA /* GoogleUtilities.framework */, @@ -497,13 +599,14 @@ path = DebugInfoTableViewController; sourceTree = ""; }; - 143BB7331F60DF0200D00541 /* MPC Flow Controllers */ = { + 143BB7331F60DF0200D00541 /* Connect View Controllers */ = { isa = PBXGroup; children = ( - 14DFBD21210EFE8E00BD8DAF /* MPCConnectViewController */, - 14DFBD22210EFE9900BD8DAF /* MPCHostViewController */, + 1414280621FC394600C48788 /* ConnectViewController.swift */, + 1414280821FC3C9800C48788 /* GameKit */, + 1414280921FC3CA000C48788 /* Multipeer Connectivity */, ); - path = "MPC Flow Controllers"; + path = "Connect View Controllers"; sourceTree = ""; }; 144A1024202FC786003DB51A /* Other */ = { @@ -534,6 +637,7 @@ 149FF8791F362BE4000A5D96 /* WikiRaces (Multi-Window) */, 14C25FDE1F6F025A00CD7373 /* WikiRaces (UI Catalog) */, 149FF88F1F362BF1000A5D96 /* WikiRacesScreenshots */, + 14E0F1A922303FD100BFF1E9 /* WikiRacesTests */, 149FF84A1F362B83000A5D96 /* Products */, 14E1B19F1F798A520082F4FA /* Frameworks */, ); @@ -546,7 +650,7 @@ 149FF8781F362BE4000A5D96 /* WikiRaces (Multi-Window).app */, 149FF88E1F362BF1000A5D96 /* WikiRacesScreenshots.xctest */, 14C25FDD1F6F025A00CD7373 /* WikiRaces (UI Catalog).app */, - 142B7A461F771278004A8AA1 /* WikiRacesCatalogScreenshots.xctest */, + 14E0F1A822303FD100BFF1E9 /* WikiRacesTests.xctest */, ); name = Products; sourceTree = ""; @@ -558,12 +662,11 @@ 143BB72F1F60D84100D00541 /* fabric.apikey */, 143BB72E1F60D84100D00541 /* fabric.buildsecret */, 143054BD1F89581100C0BC27 /* GoogleService-Info.plist */, - 14A778281F61A9A100823DE8 /* Plans.txt */, 149FF84C1F362B83000A5D96 /* AppDelegate.swift */, - 149FF8501F362B83000A5D96 /* Main.storyboard */, 149FF8531F362B83000A5D96 /* Assets.xcassets */, 149FF8551F362B83000A5D96 /* LaunchScreen.storyboard */, 149FF8581F362B83000A5D96 /* Info.plist */, + 14B8F810222C47FA006C7A06 /* GKMessageImage.png */, ); path = WikiRaces; sourceTree = ""; @@ -612,7 +715,7 @@ isa = PBXGroup; children = ( 14B55C901F3A49670090E092 /* MenuViewController */, - 143BB7331F60DF0200D00541 /* MPC Flow Controllers */, + 143BB7331F60DF0200D00541 /* Connect View Controllers */, 143948C22144CCAE00992850 /* DebugInfoTableViewController */, ); path = "Menu View Controllers"; @@ -627,13 +730,40 @@ path = Resources; sourceTree = ""; }; + 14B2DD4822212C25009B8AB3 /* MenuView */ = { + isa = PBXGroup; + children = ( + 14C4AB381F3689A90086B77F /* MenuTile.swift */, + 14B2DD3A22212298009B8AB3 /* MenuView.swift */, + 14B2DD4422212B96009B8AB3 /* MenuView+Setup.swift */, + 14B2DD402221273E009B8AB3 /* MenuView+Actions.swift */, + 1485B67C223072AB00D6800B /* MedalView.swift */, + 1485B6762230724A00D6800B /* MedalScene.swift */, + 14B4DB602224809F007D4B54 /* MovingPuzzleView.swift */, + ); + path = MenuView; + sourceTree = ""; + }; + 14B4DB6E2224FB96007D4B54 /* Cells */ = { + isa = PBXGroup; + children = ( + 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */, + 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */, + 14B4DB6A2224FA54007D4B54 /* MPCHostSoloCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; 14B55C8F1F3A495C0090E092 /* ResultsViewController */ = { isa = PBXGroup; children = ( 14C4AB2C1F3689900086B77F /* ResultsViewController.swift */, + 141E4CE62200DDD6000A0A15 /* ResultsViewController+Actions.swift */, 14C4AB301F36899B0086B77F /* ResultsViewController+TableView.swift */, 145925DD210D555D00CCC8F0 /* ResultsViewController+KB.swift */, 14C4AB341F3689A40086B77F /* ResultsTableViewCell.swift */, + 14495D8721FF9A0500CAA129 /* ResultRenderer.swift */, + 1478B1B022095360009F2F3F /* ResultRenderer+Creation.swift */, ); path = ResultsViewController; sourceTree = ""; @@ -641,11 +771,10 @@ 14B55C901F3A49670090E092 /* MenuViewController */ = { isa = PBXGroup; children = ( - 14C4AB381F3689A90086B77F /* MenuTile.swift */, + 14B2DD4822212C25009B8AB3 /* MenuView */, 14C4AB3C1F3689AE0086B77F /* MenuViewController.swift */, - 14C4AB401F3689B50086B77F /* MenuViewController+UI.swift */, - 141892F51F60ECD2006748F0 /* MenuViewController+Segue.swift */, - 143BB7171F60BDC000D00541 /* MenuViewController+Leaderboards.swift */, + 14BA538C21FE3D9100A8CB01 /* MenuViewController+Debug.swift */, + 143BB7171F60BDC000D00541 /* MenuViewController+GameKit.swift */, 142F714E210C33FC00C66558 /* MenuViewController+KB.swift */, ); path = MenuViewController; @@ -657,6 +786,7 @@ 14C4AB581F368A050086B77F /* HistoryViewController.swift */, 149357D8210E801A00F6453A /* HistoryViewController+KB.swift */, 14C4AB541F3689FE0086B77F /* HistoryTableViewCell.swift */, + 1437C51E22285FE30003E53B /* HistoryTableViewStatsCell.swift */, ); path = HistoryViewController; sourceTree = ""; @@ -677,7 +807,6 @@ children = ( 14C4AB5C1F368A0C0086B77F /* GameViewController.swift */, 14C4AB641F368A210086B77F /* GameViewController+UI.swift */, - 148B4B2D1F57519F007B70B5 /* GameViewController+Segue.swift */, 14C4AB601F368A170086B77F /* GameViewController+Manager.swift */, 142F7156210C375F00C66558 /* GameViewController+KB.swift */, ); @@ -688,7 +817,7 @@ isa = PBXGroup; children = ( 14C4AB801F368A630086B77F /* CommonExtensions.swift */, - 14C4AB7C1F368A5A0086B77F /* DurationFormatter.swift */, + 14B2DD4B22213A07009B8AB3 /* GlobalRacesHelper.swift */, 14DF31A7211627D5005BA432 /* WKRAnimationDurationConstants.swift */, 144A5AEB1F4FF5C20058CB99 /* WKRAppDelegate.swift */, ); @@ -724,10 +853,10 @@ 14DF31B121163ABA005BA432 /* Logging */ = { isa = PBXGroup; children = ( - 14E1B1991F7981C70082F4FA /* PlayerMetrics.swift */, - 1497A8541FF476BC0013E9E3 /* StateLogViewController.swift */, - 1497A8581FF478920013E9E3 /* StateLogTableViewController.swift */, - 143A8BBB1F58746800580AA2 /* StatsHelper.swift */, + 14E1B1991F7981C70082F4FA /* PlayerAnonymousMetrics.swift */, + 143A8BBB1F58746800580AA2 /* PlayerStatsManager.swift */, + 14E0F19E22303A8F00BFF1E9 /* PlayerDatabaseStat.swift */, + 141E4CF022012B2F000A0A15 /* PlayerDatabaseMetrics.swift */, ); path = Logging; sourceTree = ""; @@ -749,15 +878,24 @@ 14C6B1F01FF2EABC00F6B422 /* MPCHostViewController.swift */, 14DFBD1D210EFDEE00BD8DAF /* MPCHostViewController+Table.swift */, 142F7152210C34A300C66558 /* MPCHostViewController+KB.swift */, - 149357E2210E963800F6453A /* MPCHostPeerStateCell.swift */, - 14C6B1F31FF2EABC00F6B422 /* MPCHostSearchingCell.swift */, + 14B4DB6E2224FB96007D4B54 /* Cells */, ); path = MPCHostViewController; sourceTree = ""; }; + 14E0F1A922303FD100BFF1E9 /* WikiRacesTests */ = { + isa = PBXGroup; + children = ( + 14E0F1AA22303FD100BFF1E9 /* WikiRacesTests.swift */, + 14E0F1AC22303FD100BFF1E9 /* Info.plist */, + ); + path = WikiRacesTests; + sourceTree = ""; + }; 14E1B19F1F798A520082F4FA /* Frameworks */ = { isa = PBXGroup; children = ( + 14B8F80C222C456B006C7A06 /* GameKit.framework */, 14E1B1A01F798A520082F4FA /* CloudKit.framework */, ); name = Frameworks; @@ -784,24 +922,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 142B7A451F771278004A8AA1 /* WikiRacesCatalogScreenshots */ = { - isa = PBXNativeTarget; - buildConfigurationList = 142B7A511F771278004A8AA1 /* Build configuration list for PBXNativeTarget "WikiRacesCatalogScreenshots" */; - buildPhases = ( - 142B7A421F771278004A8AA1 /* Sources */, - 142B7A431F771278004A8AA1 /* Frameworks */, - 142B7A441F771278004A8AA1 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 142B7A4C1F771278004A8AA1 /* PBXTargetDependency */, - ); - name = WikiRacesCatalogScreenshots; - productName = WikiRacesCatalogScreenshots; - productReference = 142B7A461F771278004A8AA1 /* WikiRacesCatalogScreenshots.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; 149FF8481F362B83000A5D96 /* WikiRaces */ = { isa = PBXNativeTarget; buildConfigurationList = 149FF85B1F362B83000A5D96 /* Build configuration list for PBXNativeTarget "WikiRaces" */; @@ -878,12 +998,32 @@ dependencies = ( 14DCC5671FF2E359003AEC95 /* PBXTargetDependency */, 14DCC5651FF2E355003AEC95 /* PBXTargetDependency */, + 14584BC5220B6BE700D63428 /* PBXTargetDependency */, + 14584BCA220B6BEE00D63428 /* PBXTargetDependency */, ); name = "WikiRaces (UI Catalog)"; productName = "WikiRaces (UI Catalog)"; productReference = 14C25FDD1F6F025A00CD7373 /* WikiRaces (UI Catalog).app */; productType = "com.apple.product-type.application"; }; + 14E0F1A722303FD100BFF1E9 /* WikiRacesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 14E0F1AF22303FD100BFF1E9 /* Build configuration list for PBXNativeTarget "WikiRacesTests" */; + buildPhases = ( + 14E0F1A422303FD100BFF1E9 /* Sources */, + 14E0F1A522303FD100BFF1E9 /* Frameworks */, + 14E0F1A622303FD100BFF1E9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 14E0F1AE22303FD100BFF1E9 /* PBXTargetDependency */, + ); + name = WikiRacesTests; + productName = WikiRacesTests; + productReference = 14E0F1A822303FD100BFF1E9 /* WikiRacesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -893,19 +1033,17 @@ KnownAssetTags = ( com.apple.gamecenter.Leaderboard, ); - LastSwiftUpdateCheck = 0900; + LastSwiftUpdateCheck = 1020; LastUpgradeCheck = 1000; ORGANIZATIONNAME = "Andrew Finke"; TargetAttributes = { - 142B7A451F771278004A8AA1 = { - CreatedOnToolsVersion = 9.0; - ProvisioningStyle = Automatic; - TestTargetID = 14C25FDC1F6F025A00CD7373; - }; 149FF8481F362B83000A5D96 = { CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1000; + LastSwiftMigration = 1020; SystemCapabilities = { + com.apple.GameCenter.iOS = { + enabled = 1; + }; com.apple.Push = { enabled = 1; }; @@ -916,7 +1054,7 @@ }; 149FF8771F362BE4000A5D96 = { CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1000; + LastSwiftMigration = 1020; SystemCapabilities = { com.apple.Push = { enabled = 1; @@ -928,13 +1066,19 @@ }; 149FF88D1F362BF1000A5D96 = { CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1000; + LastSwiftMigration = 1020; TestTargetID = 149FF8481F362B83000A5D96; }; 14C25FDC1F6F025A00CD7373 = { CreatedOnToolsVersion = 9.0; + LastSwiftMigration = 1020; ProvisioningStyle = Automatic; }; + 14E0F1A722303FD100BFF1E9 = { + CreatedOnToolsVersion = 10.2; + ProvisioningStyle = Automatic; + TestTargetID = 149FF8481F362B83000A5D96; + }; }; }; buildConfigurationList = 149FF8441F362B83000A5D96 /* Build configuration list for PBXProject "WikiRaces" */; @@ -964,7 +1108,7 @@ 149FF8771F362BE4000A5D96 /* WikiRaces (Multi-Window) */, 149FF88D1F362BF1000A5D96 /* WikiRacesScreenshots */, 14C25FDC1F6F025A00CD7373 /* WikiRaces (UI Catalog) */, - 142B7A451F771278004A8AA1 /* WikiRacesCatalogScreenshots */, + 14E0F1A722303FD100BFF1E9 /* WikiRacesTests */, ); }; /* End PBXProject section */ @@ -1001,13 +1145,6 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ - 142B7A441F771278004A8AA1 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 149FF8471F362B83000A5D96 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1015,9 +1152,9 @@ 149FF8571F362B83000A5D96 /* LaunchScreen.storyboard in Resources */, 149FF8541F362B83000A5D96 /* Assets.xcassets in Resources */, 1410DB3B1F4F510900F5CAD7 /* SharedAssets.xcassets in Resources */, + 14B8F811222C47FA006C7A06 /* GKMessageImage.png in Resources */, 14DC66D11F90700A0026C6ED /* fabric.apikey in Resources */, 14A7C9B31F65A9EB00980E4D /* Settings.bundle in Resources */, - 149FF8521F362B83000A5D96 /* Main.storyboard in Resources */, 14DC66D21F90700D0026C6ED /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1029,7 +1166,6 @@ 149FF8851F362BE4000A5D96 /* LaunchScreen.storyboard in Resources */, 149FF8821F362BE4000A5D96 /* Assets.xcassets in Resources */, 149FF8801F362BE4000A5D96 /* Main-MultiWindow.storyboard in Resources */, - 1410DB371F4F504F00F5CAD7 /* Main.storyboard in Resources */, 1410DB3D1F4F510900F5CAD7 /* SharedAssets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1048,11 +1184,17 @@ 14C25FEA1F6F025A00CD7373 /* LaunchScreen.storyboard in Resources */, 14C25FE71F6F025A00CD7373 /* Assets.xcassets in Resources */, 14C25FE51F6F025A00CD7373 /* Main-Catalog.storyboard in Resources */, - 14C2600B1F6F032200CD7373 /* Main.storyboard in Resources */, 14C2600D1F6F04A200CD7373 /* SharedAssets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 14E0F1A622303FD100BFF1E9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -1119,25 +1261,20 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 142B7A421F771278004A8AA1 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 149FF8451F362B83000A5D96 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 143BB7181F60BDC000D00541 /* MenuViewController+Leaderboards.swift in Sources */, - 143A8BBC1F58746800580AA2 /* StatsHelper.swift in Sources */, + 143BB7181F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */, + 143A8BBC1F58746800580AA2 /* PlayerStatsManager.swift in Sources */, 142F714F210C33FC00C66558 /* MenuViewController+KB.swift in Sources */, 143948BD2144CC0F00992850 /* DebugInfoTableViewController.swift in Sources */, 14C4AB311F36899B0086B77F /* ResultsViewController+TableView.swift in Sources */, 144A5AEC1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */, - 14E1B19A1F7981C70082F4FA /* PlayerMetrics.swift in Sources */, + 14E1B19A1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */, 14C4AB551F3689FE0086B77F /* HistoryTableViewCell.swift in Sources */, + 14B2DD3B22212298009B8AB3 /* MenuView.swift in Sources */, + 14B4DB6B2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */, 14C4AB511F3689E20086B77F /* VotingViewController+TableView.swift in Sources */, 142F7155210C35BC00C66558 /* VotingViewController+KB.swift in Sources */, 14C4AB611F368A170086B77F /* GameViewController+Manager.swift in Sources */, @@ -1145,35 +1282,46 @@ 14C4AB591F368A050086B77F /* HistoryViewController.swift in Sources */, 14DF31A8211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */, 142F7157210C375F00C66558 /* GameViewController+KB.swift in Sources */, - 148B4B2E1F57519F007B70B5 /* GameViewController+Segue.swift in Sources */, + 1414280721FC394600C48788 /* ConnectViewController.swift in Sources */, + 1478B1B122095360009F2F3F /* ResultRenderer+Creation.swift in Sources */, 145925DE210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */, 143BB7351F60DF4A00D00541 /* MPCConnectViewController.swift in Sources */, - 14C4AB7D1F368A5A0086B77F /* DurationFormatter.swift in Sources */, + 14B4DB612224809F007D4B54 /* MovingPuzzleView.swift in Sources */, + 14B2DD4C22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */, + 14495D8821FF9A0500CAA129 /* ResultRenderer.swift in Sources */, + 141E4CE72200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */, 142F7153210C34A300C66558 /* MPCHostViewController+KB.swift in Sources */, 14C4AB811F368A630086B77F /* CommonExtensions.swift in Sources */, + 1414280B21FC437000C48788 /* GameKitConnectViewController+Match.swift in Sources */, 14C4AB851F368AED0086B77F /* VotingTableViewCell.swift in Sources */, - 141892F61F60ECD2006748F0 /* MenuViewController+Segue.swift in Sources */, + 147EF6F92202436600583D73 /* MPCHostContext.swift in Sources */, 14C4AB391F3689A90086B77F /* MenuTile.swift in Sources */, 14C6B1F61FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */, 144A102A202FC79B003DB51A /* HelpViewController.swift in Sources */, 149FF84D1F362B83000A5D96 /* AppDelegate.swift in Sources */, - 1497A8551FF476BC0013E9E3 /* StateLogViewController.swift in Sources */, 14DFBD1E210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */, 14C4AB5D1F368A0C0086B77F /* GameViewController.swift in Sources */, + 14B2DD4522212B96009B8AB3 /* MenuView+Setup.swift in Sources */, 14C4AB651F368A210086B77F /* GameViewController+UI.swift in Sources */, + 141E4CF122012B2F000A0A15 /* PlayerDatabaseMetrics.swift in Sources */, 1473E2A0210DAD7C00726377 /* HelpViewController+KB.swift in Sources */, + 1414280321FC18F600C48788 /* GameKitConnectViewController.swift in Sources */, + 14B2DD412221273E009B8AB3 /* MenuView+Actions.swift in Sources */, 141892F41F60EABC006748F0 /* MPCConnectViewController+Invite.swift in Sources */, 14C4AB2D1F3689900086B77F /* ResultsViewController.swift in Sources */, + 1485B67D223072AB00D6800B /* MedalView.swift in Sources */, + 14E0F19F22303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */, 14C4AB491F3689C60086B77F /* VotingViewController.swift in Sources */, 149357D9210E801A00F6453A /* HistoryViewController+KB.swift in Sources */, + 1485B6772230724A00D6800B /* MedalScene.swift in Sources */, 14C4AB3D1F3689AE0086B77F /* MenuViewController.swift in Sources */, + 1437C51F22285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */, 149357E3210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */, 142A09BE210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */, - 14C4AB411F3689B50086B77F /* MenuViewController+UI.swift in Sources */, 144A1029202FC79B003DB51A /* CenteredTableViewController.swift in Sources */, 149357DF210E934300F6453A /* MPCConnectViewController+UI.swift in Sources */, 143948C12144CC8C00992850 /* DebugInfoTableViewCell.swift in Sources */, - 1497A8591FF478920013E9E3 /* StateLogTableViewController.swift in Sources */, + 14BA538D21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */, 14C6B1F41FF2EABD00F6B422 /* MPCHostViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1184,45 +1332,58 @@ files = ( 14DF31B721169290005BA432 /* MPCConnectViewController.swift in Sources */, 14C4AB331F36899B0086B77F /* ResultsViewController+TableView.swift in Sources */, - 1497A8561FF476BC0013E9E3 /* StateLogViewController.swift in Sources */, - 1497A85A1FF478920013E9E3 /* StateLogTableViewController.swift in Sources */, 14C4AB571F3689FE0086B77F /* HistoryTableViewCell.swift in Sources */, 14891AA6214F6BDB001BDEB8 /* DebugInfoTableViewCell.swift in Sources */, + 14E0F1A022303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */, 14C4AB531F3689E20086B77F /* VotingViewController+TableView.swift in Sources */, 14DF31B321169232005BA432 /* MPCConnectViewController+KB.swift in Sources */, 14C4AB631F368A170086B77F /* GameViewController+Manager.swift in Sources */, - 143BB7151F60AEC900D00541 /* StatsHelper.swift in Sources */, - 143BB7191F60BDC000D00541 /* MenuViewController+Leaderboards.swift in Sources */, + 141E4CE82200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */, + 143BB7151F60AEC900D00541 /* PlayerStatsManager.swift in Sources */, + 143BB7191F60BDC000D00541 /* MenuViewController+GameKit.swift in Sources */, 14DF31A9211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */, 145925DF210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */, 149357DA210E801A00F6453A /* HistoryViewController+KB.swift in Sources */, + 1485B6782230724A00D6800B /* MedalScene.swift in Sources */, 14C4AB371F3689A40086B77F /* ResultsTableViewCell.swift in Sources */, - 148B4B2F1F57519F007B70B5 /* GameViewController+Segue.swift in Sources */, 14DF31B62116923C005BA432 /* MPCHostViewController.swift in Sources */, + 14B2DD4622212B96009B8AB3 /* MenuView+Setup.swift in Sources */, + 14B4DB662224F1B9007D4B54 /* GameKitConnectViewController.swift in Sources */, 14C4AB5B1F368A050086B77F /* HistoryViewController.swift in Sources */, + 14DD97282202294D00AAB389 /* PlayerDatabaseMetrics.swift in Sources */, 149FF87D1F362BE4000A5D96 /* ViewController.swift in Sources */, - 14C4AB7F1F368A5A0086B77F /* DurationFormatter.swift in Sources */, + 14B2DD422221273E009B8AB3 /* MenuView+Actions.swift in Sources */, + 14B4DB682224F1BC007D4B54 /* GameKitConnectViewController+Match.swift in Sources */, 14DF31B421169236005BA432 /* MPCConnectViewController+UI.swift in Sources */, + 14495D8921FF9A0500CAA129 /* ResultRenderer.swift in Sources */, 14C4AB831F368A630086B77F /* CommonExtensions.swift in Sources */, - 14E1B19B1F7981C70082F4FA /* PlayerMetrics.swift in Sources */, + 147EF6FA2202436600583D73 /* MPCHostContext.swift in Sources */, + 14E1B19B1F7981C70082F4FA /* PlayerAnonymousMetrics.swift in Sources */, 14C4AB871F368AED0086B77F /* VotingTableViewCell.swift in Sources */, + 14B2DD3C22212298009B8AB3 /* MenuView.swift in Sources */, + 1478B1B222095360009F2F3F /* ResultRenderer+Creation.swift in Sources */, + 1437C52022285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */, + 14B4DB6C2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */, + 14BA538E21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */, + 14DD97252202293900AAB389 /* ConnectViewController.swift in Sources */, 14C4AB3B1F3689A90086B77F /* MenuTile.swift in Sources */, 149FF87B1F362BE4000A5D96 /* AppDelegate.swift in Sources */, 14C4AB5F1F368A0C0086B77F /* GameViewController.swift in Sources */, + 1485B67E223072AB00D6800B /* MedalView.swift in Sources */, 149357E4210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */, 14C4AB671F368A210086B77F /* GameViewController+UI.swift in Sources */, - 141892F71F60ECD2006748F0 /* MenuViewController+Segue.swift in Sources */, 14B55C991F3A49D20090E092 /* DebugWindow.swift in Sources */, 144A5AED1F4FF5C20058CB99 /* WKRAppDelegate.swift in Sources */, 144A102B202FC7A1003DB51A /* CenteredTableViewController.swift in Sources */, + 14B4DB622224809F007D4B54 /* MovingPuzzleView.swift in Sources */, 14C4AB2F1F3689900086B77F /* ResultsViewController.swift in Sources */, 14C4AB4B1F3689C60086B77F /* VotingViewController.swift in Sources */, 14DF31B521169239005BA432 /* MPCHostViewController+Table.swift in Sources */, 14DF31BA21169371005BA432 /* MPCConnectViewController+Invite.swift in Sources */, 14C4AB3F1F3689AE0086B77F /* MenuViewController.swift in Sources */, + 14B2DD4D22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */, 14DF31B921169360005BA432 /* MPCHostSearchingCell.swift in Sources */, 14891AA9214F6BDE001BDEB8 /* DebugInfoTableViewController.swift in Sources */, - 14C4AB431F3689B50086B77F /* MenuViewController+UI.swift in Sources */, 144A102C202FC7A1003DB51A /* HelpViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1241,55 +1402,83 @@ buildActionMask = 2147483647; files = ( 14C25FE21F6F025A00CD7373 /* ViewController.swift in Sources */, - 14C25FF41F6F028100CD7373 /* MenuViewController+Segue.swift in Sources */, + 147EF7122202D76000583D73 /* MPCHostContext.swift in Sources */, 145925E0210D555D00CCC8F0 /* ResultsViewController+KB.swift in Sources */, 14DF31AA211627D5005BA432 /* WKRAnimationDurationConstants.swift in Sources */, + 14E0F1A122303A8F00BFF1E9 /* PlayerDatabaseStat.swift in Sources */, 149357DB210E801A00F6453A /* HistoryViewController+KB.swift in Sources */, 14C25FFC1F6F02A500CD7373 /* GameViewController+UI.swift in Sources */, 14DF31B821169291005BA432 /* MPCConnectViewController.swift in Sources */, 14C25FF11F6F028100CD7373 /* MenuTile.swift in Sources */, 14C25FFE1F6F02A500CD7373 /* GameViewController+Manager.swift in Sources */, - 14C260071F6F02A500CD7373 /* StatsHelper.swift in Sources */, + 14495D8A21FF9A0500CAA129 /* ResultRenderer.swift in Sources */, + 14C260071F6F02A500CD7373 /* PlayerStatsManager.swift in Sources */, 14C260091F6F02A500CD7373 /* CommonExtensions.swift in Sources */, 14C260011F6F02A500CD7373 /* ResultsTableViewCell.swift in Sources */, + 1485B6792230724A00D6800B /* MedalScene.swift in Sources */, 14C25FFB1F6F02A500CD7373 /* GameViewController.swift in Sources */, + 14B2DD4722212B96009B8AB3 /* MenuView+Setup.swift in Sources */, 14C25FFF1F6F02A500CD7373 /* ResultsViewController.swift in Sources */, - 14C25FF31F6F028100CD7373 /* MenuViewController+UI.swift in Sources */, - 14C260081F6F02A500CD7373 /* DurationFormatter.swift in Sources */, + 14B4DB672224F1BA007D4B54 /* GameKitConnectViewController.swift in Sources */, + 14B2DD432221273E009B8AB3 /* MenuView+Actions.swift in Sources */, + 14BA538F21FE3D9100A8CB01 /* MenuViewController+Debug.swift in Sources */, 149357E5210E963800F6453A /* MPCHostPeerStateCell.swift in Sources */, + 14DD97292202295100AAB389 /* PlayerDatabaseMetrics.swift in Sources */, + 14B4DB692224F1BD007D4B54 /* GameKitConnectViewController+Match.swift in Sources */, 142A09C0210EEBD000979C46 /* MPCConnectViewController+KB.swift in Sources */, 14C25FE01F6F025A00CD7373 /* AppDelegate.swift in Sources */, 14DFBD20210EFDEE00BD8DAF /* MPCHostViewController+Table.swift in Sources */, 14C2600A1F6F02A500CD7373 /* WKRAppDelegate.swift in Sources */, + 14B2DD3D22212298009B8AB3 /* MenuView.swift in Sources */, + 1480F6C621FB77DE00081F58 /* DebugInfoTableViewCell.swift in Sources */, + 1480F6C321FB77D300081F58 /* DebugInfoTableViewController.swift in Sources */, 14D8AD481F81835700914E5A /* DebugWindow.swift in Sources */, + 1437C52122285FE30003E53B /* HistoryTableViewStatsCell.swift in Sources */, + 14B4DB6D2224FA54007D4B54 /* MPCHostSoloCell.swift in Sources */, + 1478B1B322095360009F2F3F /* ResultRenderer+Creation.swift in Sources */, 14C260031F6F02A500CD7373 /* HistoryViewController.swift in Sources */, + 141E4CE92200DDD6000A0A15 /* ResultsViewController+Actions.swift in Sources */, 14C6B1F71FF2EABD00F6B422 /* MPCHostSearchingCell.swift in Sources */, 14C260021F6F02A500CD7373 /* HistoryTableViewCell.swift in Sources */, + 1485B67F223072AB00D6800B /* MedalView.swift in Sources */, 149357E1210E934300F6453A /* MPCConnectViewController+UI.swift in Sources */, 144A102E202FC7A2003DB51A /* HelpViewController.swift in Sources */, - 14D8AD471F81828D00914E5A /* PlayerMetrics.swift in Sources */, - 1497A8571FF476BC0013E9E3 /* StateLogViewController.swift in Sources */, + 14D8AD471F81828D00914E5A /* PlayerAnonymousMetrics.swift in Sources */, 14C25FF21F6F028100CD7373 /* MenuViewController.swift in Sources */, 14C260001F6F02A500CD7373 /* ResultsViewController+TableView.swift in Sources */, + 14B4DB632224809F007D4B54 /* MovingPuzzleView.swift in Sources */, 14C260041F6F02A500CD7373 /* VotingViewController.swift in Sources */, 14C260051F6F02A500CD7373 /* VotingViewController+TableView.swift in Sources */, - 14C25FF51F6F028100CD7373 /* MenuViewController+Leaderboards.swift in Sources */, + 14C25FF51F6F028100CD7373 /* MenuViewController+GameKit.swift in Sources */, 14C260061F6F02A500CD7373 /* VotingTableViewCell.swift in Sources */, 14DF31BB21169372005BA432 /* MPCConnectViewController+Invite.swift in Sources */, - 14C25FFD1F6F02A500CD7373 /* GameViewController+Segue.swift in Sources */, + 14B2DD4E22213A07009B8AB3 /* GlobalRacesHelper.swift in Sources */, 144A102D202FC7A2003DB51A /* CenteredTableViewController.swift in Sources */, - 1497A85B1FF478920013E9E3 /* StateLogTableViewController.swift in Sources */, + 14BA538921FE3B1400A8CB01 /* ConnectViewController.swift in Sources */, 14C6B1F51FF2EABD00F6B422 /* MPCHostViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 14E0F1A422303FD100BFF1E9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 14E0F1AB22303FD100BFF1E9 /* WikiRacesTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 142B7A4C1F771278004A8AA1 /* PBXTargetDependency */ = { + 14584BC5220B6BE700D63428 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 14C25FDC1F6F025A00CD7373 /* WikiRaces (UI Catalog) */; - targetProxy = 142B7A4B1F771278004A8AA1 /* PBXContainerItemProxy */; + name = WKRKit; + targetProxy = 14584BC4220B6BE700D63428 /* PBXContainerItemProxy */; + }; + 14584BCA220B6BEE00D63428 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = WKRUIKit; + targetProxy = 14584BC9220B6BEE00D63428 /* PBXContainerItemProxy */; }; 149FF8941F362BF1000A5D96 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -1316,6 +1505,11 @@ name = WKRUIKit; targetProxy = 14DCC5661FF2E359003AEC95 /* PBXContainerItemProxy */; }; + 14E0F1AE22303FD100BFF1E9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 149FF8481F362B83000A5D96 /* WikiRaces */; + targetProxy = 14E0F1AD22303FD100BFF1E9 /* PBXContainerItemProxy */; + }; 14E6D5601F86A1CA005EB3B9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = WKRKit; @@ -1329,14 +1523,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 149FF8501F362B83000A5D96 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 149FF8511F362B83000A5D96 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 149FF8551F362B83000A5D96 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -1380,36 +1566,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 142B7A4D1F771278004A8AA1 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 72S993BNAV; - INFOPLIST_FILE = WikiRacesCatalogScreenshots/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WikiRacesCatalogScreenshots; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "WikiRaces (UI Catalog)"; - }; - name = Debug; - }; - 142B7A4E1F771278004A8AA1 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 72S993BNAV; - INFOPLIST_FILE = WikiRacesCatalogScreenshots/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WikiRacesCatalogScreenshots; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "WikiRaces (UI Catalog)"; - }; - name = Release; - }; 149FF8591F362B83000A5D96 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1443,7 +1599,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1531,6 +1687,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WikiRaces/WikiRaces.entitlements; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 72S993BNAV; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1545,7 +1702,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.wikiraces; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = Shared/Frameworks/Analytics/BridgingHeader.h; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1556,6 +1713,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WikiRaces/WikiRaces.entitlements; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 72S993BNAV; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1569,7 +1727,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.wikiraces; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = Shared/Frameworks/Analytics/BridgingHeader.h; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1587,7 +1745,7 @@ OTHER_SWIFT_FLAGS = "-DMULTIWINDOWDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WikiRaces--Multi-Window-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 2; }; name = Debug; @@ -1605,7 +1763,7 @@ OTHER_SWIFT_FLAGS = "-DMULTIWINDOWDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WikiRaces--Multi-Window-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 2; }; name = Release; @@ -1618,7 +1776,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WikiRacesScreenshots; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = WikiRaces; }; @@ -1632,7 +1790,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WikiRacesScreenshots; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = WikiRaces; }; @@ -1644,6 +1802,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 72S993BNAV; INFOPLIST_FILE = "WikiRaces (UI Catalog)/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -1651,7 +1810,7 @@ OTHER_SWIFT_FLAGS = "-DMULTIWINDOWDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WikiRaces--UI-Catalog-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1669,23 +1828,54 @@ OTHER_SWIFT_FLAGS = "-DMULTIWINDOWDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.andrewfinke.WikiRaces--UI-Catalog-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 14E0F1B022303FD100BFF1E9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 72S993BNAV; + INFOPLIST_FILE = WikiRacesTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WikiRacesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WikiRaces.app/WikiRaces"; + }; + name = Debug; + }; + 14E0F1B122303FD100BFF1E9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 72S993BNAV; + INFOPLIST_FILE = WikiRacesTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.andrewfinke.WikiRacesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WikiRaces.app/WikiRaces"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 142B7A511F771278004A8AA1 /* Build configuration list for PBXNativeTarget "WikiRacesCatalogScreenshots" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 142B7A4D1F771278004A8AA1 /* Debug */, - 142B7A4E1F771278004A8AA1 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 149FF8441F362B83000A5D96 /* Build configuration list for PBXProject "WikiRaces" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1731,6 +1921,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 14E0F1AF22303FD100BFF1E9 /* Build configuration list for PBXNativeTarget "WikiRacesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 14E0F1B022303FD100BFF1E9 /* Debug */, + 14E0F1B122303FD100BFF1E9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 149FF8411F362B83000A5D96 /* Project object */; diff --git a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces-MW.xcscheme b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces (Multi-Window).xcscheme similarity index 99% rename from WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces-MW.xcscheme rename to WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces (Multi-Window).xcscheme index 027396a..822aa24 100644 --- a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces-MW.xcscheme +++ b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRaces (Multi-Window).xcscheme @@ -1,6 +1,6 @@
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesScreenshots.xcscheme b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesScreenshots.xcscheme index 52192dc..c2b6525 100644 --- a/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesScreenshots.xcscheme +++ b/WikiRaces/WikiRaces.xcodeproj/xcshareddata/xcschemes/WikiRacesScreenshots.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WikiRaces/WikiRaces/AppDelegate.swift b/WikiRaces/WikiRaces/AppDelegate.swift index 2a24890..83ab37b 100644 --- a/WikiRaces/WikiRaces/AppDelegate.swift +++ b/WikiRaces/WikiRaces/AppDelegate.swift @@ -9,6 +9,7 @@ import CloudKit import UIKit +import WKRKit import WKRUIKit import Crashlytics @@ -20,25 +21,43 @@ internal class AppDelegate: WKRAppDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - #if !DEBUG guard let url = Bundle.main.url(forResource: "fabric.apikey", withExtension: nil), let key = try? String(contentsOf: url).replacingOccurrences(of: "\n", with: "") else { fatalError("Failed to get API keys") } Crashlytics.start(withAPIKey: key) - #endif FirebaseApp.configure() - StatsHelper.shared.start() configureConstants() configureAppearance() + NotificationCenter.default.addObserver(self, + selector: #selector(showBanHammer), + name: PlayerDatabaseMetrics.banHammerNotification, + object: nil) + + PlayerStatsManager.shared.start() + PlayerDatabaseMetrics.shared.connect() + logCloudStatus() logInterfaceMode() logBuild() + cleanTempDirectory() + + if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") { + UIView.setAnimationsEnabled(false) + } + + window = UIWindow(frame: UIScreen.main.bounds) + let controller = MenuViewController() + let nav = UINavigationController(rootViewController: controller) + nav.setNavigationBarHidden(true, animated: false) + window?.rootViewController = nav + window?.makeKeyAndVisible() + return true } @@ -46,22 +65,40 @@ internal class AppDelegate: WKRAppDelegate { private func logCloudStatus() { CKContainer.default().accountStatus { (status, _) in - PlayerMetrics.log(event: .cloudStatus, + PlayerAnonymousMetrics.log(event: .cloudStatus, attributes: ["CloudStatus": status.rawValue.description]) } } private func logInterfaceMode() { let mode = WKRUIStyle.isDark ? "Dark" : "Light" - PlayerMetrics.log(event: .interfaceMode, attributes: ["Mode": mode]) + PlayerAnonymousMetrics.log(event: .interfaceMode, attributes: ["Mode": mode]) } private func logBuild() { - guard let bundleBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, - let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { - fatalError("No bundle info dictionary") - } - PlayerMetrics.log(event: .buildInfo(version: bundleVersion, build: bundleBuild)) + let appInfo = Bundle.main.appInfo + let metrics = PlayerDatabaseMetrics.shared + metrics.log(value: appInfo.version, for: "coreVersion") + metrics.log(value: appInfo.build.description, for: "coreBuild") + metrics.log(value: WKRKitConstants.current.version.description, + for: "WKRKitConstantsVersion") + metrics.log(value: WKRUIKitConstants.current.version.description, + for: "WKRUIKitConstantsVersion") + metrics.log(value: UIDevice.current.systemVersion, + for: "osVersion") + } + + @objc + func showBanHammer() { + let controller = UIAlertController(title: "You have been banned from WikiRaces", + message: nil, + preferredStyle: .alert) + + window?.rootViewController?.present(controller, + animated: true, + completion: nil) + + PlayerAnonymousMetrics.log(event: .banHammer) } } diff --git a/WikiRaces/WikiRaces/Base.lproj/Main.storyboard b/WikiRaces/WikiRaces/Base.lproj/Main.storyboard deleted file mode 100644 index 3637768..0000000 --- a/WikiRaces/WikiRaces/Base.lproj/Main.storyboard +++ /dev/null @@ -1,465 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WikiRaces/WikiRaces/GKMessageImage.png b/WikiRaces/WikiRaces/GKMessageImage.png new file mode 100644 index 0000000..4543b60 Binary files /dev/null and b/WikiRaces/WikiRaces/GKMessageImage.png differ diff --git a/WikiRaces/WikiRaces/Info.plist b/WikiRaces/WikiRaces/Info.plist index ae14b29..8f350dc 100644 --- a/WikiRaces/WikiRaces/Info.plist +++ b/WikiRaces/WikiRaces/Info.plist @@ -15,20 +15,21 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.6.2 + 3.6.4 CFBundleVersion - 3265 + 5598 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS + NSPhotoLibraryAddUsageDescription + WikiRaces needs your permission to save the race result image to your photo library. UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 + gamekit UISupportedInterfaceOrientations diff --git a/WikiRaces/WikiRaces/Plans.txt b/WikiRaces/WikiRaces/Plans.txt deleted file mode 100644 index 472b4b2..0000000 --- a/WikiRaces/WikiRaces/Plans.txt +++ /dev/null @@ -1,29 +0,0 @@ -WikiRaces 3.0 Plans: - -3.0 (September) - -- Complete rewrite (Core + Kits) -- Cloud syncing -- Join match mid session -- Network protocol for adding new services -- 2x final articles -- Better final article verification -- Ability to reload page -- Better error messages - -3.1 (October) - -- Ability to remotely update WKKitConstants -- Ability to remotely update final article list -- Ability to remotely update js page modifiers -- New leaderboards (Total time / Fastest Time) - -3.X (TBD) - -- Bug Fixes -- Improve error messages -- General polish / cleanup -- Better internal documentation -- Fix rare retain cycles -- Remove iOS 10.0 -- Fix broken tests from remote update diff --git a/WikiRaces/WikiRacesScreenshots/SnapshotHelper.swift b/WikiRaces/WikiRacesScreenshots/SnapshotHelper.swift index 5e4a9c3..beec60b 100755 --- a/WikiRaces/WikiRacesScreenshots/SnapshotHelper.swift +++ b/WikiRaces/WikiRacesScreenshots/SnapshotHelper.swift @@ -3,7 +3,6 @@ // Example // // Created by Felix Krause on 10/8/15. -// Copyright © 2015 Felix Krause. All rights reserved. // // ----------------------------------------------------- @@ -14,23 +13,30 @@ // ----------------------------------------------------- //swiftlint:disable line_length + import Foundation import XCTest var deviceLanguage = "" var locale = "" -@available(*, deprecated, message: "use setupSnapshot: instead") -func setLanguage(_ app: XCUIApplication) { - setupSnapshot(app) -} - func setupSnapshot(_ app: XCUIApplication) { Snapshot.setupSnapshot(app) } -func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) { - Snapshot.snapshot(name, waitForLoadingIndicator: waitForLoadingIndicator) +func snapshot(_ name: String, waitForLoadingIndicator: Bool) { + if waitForLoadingIndicator { + Snapshot.snapshot(name) + } else { + Snapshot.snapshot(name, timeWaitingForIdle: 0) + } +} + +/// - Parameters: +/// - name: The name of the snapshot +/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. +func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + Snapshot.snapshot(name, timeWaitingForIdle: timeout) } enum SnapshotError: Error, CustomDebugStringConvertible { @@ -38,6 +44,7 @@ enum SnapshotError: Error, CustomDebugStringConvertible { case cannotFindHomeDirectory case cannotFindSimulatorHomeDirectory case cannotAccessSimulatorHomeDirectory(String) + case cannotRunOnPhysicalDevice var debugDescription: String { switch self { @@ -49,22 +56,27 @@ enum SnapshotError: Error, CustomDebugStringConvertible { return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome): return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?" + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." } } } +@objcMembers open class Snapshot: NSObject { - static var app: XCUIApplication! - static var cacheDirectory: URL! + static var app: XCUIApplication? + static var cacheDirectory: URL? static var screenshotsDirectory: URL? { - return cacheDirectory.appendingPathComponent("screenshots", isDirectory: true) + return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) } open class func setupSnapshot(_ app: XCUIApplication) { + + Snapshot.app = app + do { let cacheDir = try pathPrefix() Snapshot.cacheDirectory = cacheDir - Snapshot.app = app setLanguage(app) setLocale(app) setLaunchArguments(app) @@ -74,6 +86,11 @@ open class Snapshot: NSObject { } class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + let path = cacheDirectory.appendingPathComponent("language.txt") do { @@ -86,6 +103,11 @@ open class Snapshot: NSObject { } class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + let path = cacheDirectory.appendingPathComponent("locale.txt") do { @@ -94,13 +116,22 @@ open class Snapshot: NSObject { } catch { print("Couldn't detect/set locale...") } + if locale.isEmpty { locale = Locale(identifier: deviceLanguage).identifier } - app.launchArguments += ["-AppleLocale", "\"\(locale)\""] + + if !locale.isEmpty { + app.launchArguments += ["-AppleLocale", "\"\(locale)\""] + } } class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] @@ -117,19 +148,26 @@ open class Snapshot: NSObject { } } - open class func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) { - if waitForLoadingIndicator { - waitForLoadingIndicatorToDisappear() + open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + if timeout > 0 { + waitForLoadingIndicatorToDisappear(within: timeout) } - print("snapshot: \(name)") // more information about this, check out https://github.com/fastlane/fastlane/tree/master/snapshot#how-does-it-work + print("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work sleep(1) // Waiting for the animation to be finished (kind of) #if os(OSX) XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) #else - let screenshot = app.windows.firstMatch.screenshot() + + guard let app = self.app else { + print("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let window = app.windows.firstMatch + let screenshot = window.screenshot() guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") do { @@ -141,17 +179,14 @@ open class Snapshot: NSObject { #endif } - class func waitForLoadingIndicatorToDisappear() { + class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { #if os(tvOS) return #endif - let query = XCUIApplication().statusBars.children(matching: .other).element(boundBy: 1).children(matching: .other) - - while (0.. URL? { @@ -163,34 +198,85 @@ open class Snapshot: NSObject { throw SnapshotError.cannotDetectUser } - guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { + guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { throw SnapshotError.cannotFindHomeDirectory } homeDir = usersDir.appendingPathComponent(user) #else - guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { - throw SnapshotError.cannotFindSimulatorHomeDirectory - } - guard let homeDirUrl = URL(string: simulatorHostHome) else { - throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) - } - homeDir = URL(fileURLWithPath: homeDirUrl.path) + #if arch(i386) || arch(x86_64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + guard let homeDirUrl = URL(string: simulatorHostHome) else { + throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) + } + homeDir = URL(fileURLWithPath: homeDirUrl.path) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif #endif return homeDir.appendingPathComponent("Library/Caches/tools.fastlane") } } -extension XCUIElement { - var isLoadingIndicator: Bool { - let whiteListedLoaders = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] - if whiteListedLoaders.contains(self.identifier) { - return false +private extension XCUIElementAttributes { + var isNetworkLoadingIndicator: Bool { + if hasWhiteListedIdentifier { return false } + + let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) + let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) + + return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize + } + + var hasWhiteListedIdentifier: Bool { + let whiteListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] + + return whiteListedIdentifiers.contains(identifier) + } + + func isStatusBar(_ deviceWidth: CGFloat) -> Bool { + if elementType == .statusBar { return true } + guard frame.origin == .zero else { return false } + + let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) + let newStatusBarSize = CGSize(width: deviceWidth, height: 44) + + return [oldStatusBarSize, newStatusBarSize].contains(frame.size) + } +} + +private extension XCUIElementQuery { + var networkLoadingIndicators: XCUIElementQuery { + let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isNetworkLoadingIndicator + } + + return self.containing(isNetworkLoadingIndicator) + } + + var deviceStatusBars: XCUIElementQuery { + let deviceWidth = XCUIApplication().windows.firstMatch.frame.width + + let isStatusBar = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isStatusBar(deviceWidth) } - return self.frame.size == CGSize(width: 10, height: 20) + + return self.containing(isStatusBar) + } +} + +private extension CGFloat { + func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { + return numberA...numberB ~= self } } // Please don't remove the lines below // They are used to detect outdated configuration files -// SnapshotHelperVersion [1.5] +// SnapshotHelperVersion [1.13] diff --git a/WikiRaces/WikiRacesScreenshots/WikiRacesScreenshots.swift b/WikiRaces/WikiRacesScreenshots/WikiRacesScreenshots.swift index c013f2a..f83bccb 100644 --- a/WikiRaces/WikiRacesScreenshots/WikiRacesScreenshots.swift +++ b/WikiRaces/WikiRacesScreenshots/WikiRacesScreenshots.swift @@ -28,13 +28,10 @@ class WikiRacesScreenshots: XCTestCase { func testExample() { sleep(5) XCUIDevice.shared.orientation = .landscapeLeft - sleep(2) - snapshot("Land") - XCUIDevice.shared.orientation = .portrait - sleep(2) - snapshot("Por") - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. + snapshot("1_portrait") + XCUIApplication().buttons["GLOBAL RACE"].tap() + sleep(5) + snapshot("2_game") } } diff --git a/WikiRaces/WikiRacesTests/Info.plist b/WikiRaces/WikiRacesTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/WikiRaces/WikiRacesTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/WikiRaces/WikiRacesTests/WikiRacesTests.swift b/WikiRaces/WikiRacesTests/WikiRacesTests.swift new file mode 100644 index 0000000..855f706 --- /dev/null +++ b/WikiRaces/WikiRacesTests/WikiRacesTests.swift @@ -0,0 +1,233 @@ +// +// WikiRacesTests.swift +// WikiRacesTests +// +// Created by Andrew Finke on 3/6/19. +// Copyright © 2019 Andrew Finke. All rights reserved. +// + +import XCTest +@testable import WikiRaces + +class WikiRacesTests: XCTestCase { + + override func setUp() { + if let bundleID = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: bundleID) + for key in NSUbiquitousKeyValueStore.default.dictionaryRepresentation.keys { + NSUbiquitousKeyValueStore.default.removeObject(forKey: key) + } + } + } + + override func tearDown() { + if let bundleID = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: bundleID) + for key in NSUbiquitousKeyValueStore.default.dictionaryRepresentation.keys { + NSUbiquitousKeyValueStore.default.removeObject(forKey: key) + } + } + } + + func testMenuStats() { + let menuView = MenuView() + + var stat = PlayerDatabaseStat.mpcPressedJoin + var value = stat.value() + menuView.joinLocalRace() + XCTAssertEqual(value + 1, stat.value()) + + stat = PlayerDatabaseStat.mpcPressedHost + value = stat.value() + menuView.createLocalRace() + XCTAssertEqual(value + 1, stat.value()) + + stat = PlayerDatabaseStat.gkPressedJoin + value = stat.value() + menuView.joinGlobalRace() + XCTAssertEqual(value + 1, stat.value()) + } + + func testViewedPage() { + for raceIndex in 1...90 { + guard let raceType = PlayerStatsManager.RaceType(rawValue: (raceIndex % 3) + 1) else { + XCTFail("race type nil") + return + } + let pageStat: PlayerDatabaseStat + + switch raceType { + case .mpc: + pageStat = .mpcPages + case .gameKit: + pageStat = .gkPages + case .solo: + pageStat = .soloPages + } + + let value = Int(pageStat.value()) + PlayerStatsManager.shared.viewedPage(raceType: raceType) + XCTAssertEqual(value + 1, Int(pageStat.value())) + } + } + + func testConnected() { + for raceIndex in 4...40 { + guard let raceType = PlayerStatsManager.RaceType(rawValue: (raceIndex % 2) + 1) else { + XCTFail("race type nil") + return + } + + let playersKey: String + var uniqueStat: PlayerDatabaseStat + var totalStat: PlayerDatabaseStat + switch raceType { + case .mpc: + playersKey = "PlayersArray" + uniqueStat = .mpcUniquePlayers + totalStat = .mpcTotalPlayers + case .gameKit: + playersKey = "GKPlayersArray" + uniqueStat = .gkUniquePlayers + totalStat = .gkTotalPlayers + default: return + } + + var existingPlayers = UserDefaults.standard.stringArray(forKey: playersKey) ?? [] + let maxPlayers = Int.random(in: 2..<(raceIndex * 2)) + let newPlayers = (1..() + for raceIndex in 0..<600 { + guard let raceType = PlayerStatsManager.RaceType(rawValue: (raceIndex % 3) + 1) else { + XCTFail("race type nil") + return + } + + let newPlace = Double(Int.random(in: 1..<5)) + let newPoints = Double(Int.random(in: 0...10)) + let newTimeRaced = Double(Int.random(in: 0...100)) + let newPixelsScrolled = Double(Int.random(in: 0...100000)) + + let raceCountStat: PlayerDatabaseStat + let racePointsStat: PlayerDatabaseStat + let racePlaceStat: PlayerDatabaseStat + let raceTimeStat: PlayerDatabaseStat + let racePixelsScrolledStat: PlayerDatabaseStat + + switch raceType { + + case .mpc: + raceCountStat = .mpcRaces + racePointsStat = .mpcPoints + raceTimeStat = .mpcTotalTime + racePixelsScrolledStat = .mpcPixelsScrolled + if newPlace == 1 { + racePlaceStat = .mpcRaceFinishFirst + } else if newPlace == 2 { + racePlaceStat = .mpcRaceFinishSecond + } else if newPlace == 3 { + racePlaceStat = .mpcRaceFinishThird + } else { + racePlaceStat = .mpcRaceDNF + } + case .gameKit: + raceCountStat = .gkRaces + racePointsStat = .gkPoints + raceTimeStat = .gkTotalTime + racePixelsScrolledStat = .gkPixelsScrolled + if newPlace == 1 { + racePlaceStat = .gkRaceFinishFirst + } else if newPlace == 2 { + racePlaceStat = .gkRaceFinishSecond + } else if newPlace == 3 { + racePlaceStat = .gkRaceFinishThird + } else { + racePlaceStat = .gkRaceDNF + } + case .solo: + raceCountStat = .soloRaces + raceTimeStat = .soloTotalTime + racePixelsScrolledStat = .soloPixelsScrolled + + // N/A for solo + racePointsStat = .soloHelp + racePlaceStat = .soloHelp + } + + let races = raceCountStat.value() + let points = racePointsStat.value() + let place = racePlaceStat.value() + let timeRaced = raceTimeStat.value() + let racePixelsScrolled = racePixelsScrolledStat.value() + + PlayerStatsManager.shared.completedRace(type: raceType, + points: Int(newPoints), + place: Int(newPlace), + timeRaced: Int(newTimeRaced), + pixelsScrolled: Int(newPixelsScrolled)) + + if raceType != .solo { + XCTAssertEqual(points + newPoints, racePointsStat.value()) + if newPlace <= 3 { + XCTAssertEqual(place + 1, racePlaceStat.value()) + } + } + XCTAssertEqual(races + 1, raceCountStat.value()) + XCTAssertEqual(racePixelsScrolled + newPixelsScrolled, racePixelsScrolledStat.value()) + XCTAssertEqual(timeRaced + newTimeRaced, raceTimeStat.value()) + testedStats = testedStats.union([ + raceCountStat, + racePointsStat, + racePlaceStat, + raceTimeStat, + racePixelsScrolledStat + ]) + } + print("Tested: " + testedStats.map({ $0.rawValue }).sorted().description) + } +} diff --git a/WikiRaces/fabric.apikey b/WikiRaces/fabric.apikey new file mode 100644 index 0000000..e69de29 diff --git a/WikiRaces/fabric.buildsecret b/WikiRaces/fabric.buildsecret new file mode 100644 index 0000000..e69de29 diff --git a/WikiRaces/Deliverfile b/WikiRaces/fastlane/Deliverfile similarity index 100% rename from WikiRaces/Deliverfile rename to WikiRaces/fastlane/Deliverfile diff --git a/WikiRaces/fastlane/Fastfile b/WikiRaces/fastlane/Fastfile new file mode 100644 index 0000000..01aeb4b --- /dev/null +++ b/WikiRaces/fastlane/Fastfile @@ -0,0 +1,25 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Push a new beta build to TestFlight" + lane :beta do + build_app(scheme: "WikiRaces", analyze_build_time: true) + changelog_from_git_commits(pretty:"[%ad] %s", date_format:"short") + upload_to_testflight(distribute_external:true, notify_external_testers:false) + end +end diff --git a/WikiRaces/Snapfile b/WikiRaces/fastlane/Snapfile similarity index 86% rename from WikiRaces/Snapfile rename to WikiRaces/fastlane/Snapfile index 8fbdd06..4b4d029 100644 --- a/WikiRaces/Snapfile +++ b/WikiRaces/fastlane/Snapfile @@ -2,12 +2,15 @@ # A list of devices you want to take the screenshots from devices([ -"iPhone X", +"iPhone Xs Max", +"iPhone Xs", "iPhone 8", "iPhone 8 Plus", "iPad Pro (9.7-inch)", "iPad Pro (10.5-inch)", -"iPad Pro (12.9-inch)" +"iPad Pro (11-inch)", +"iPad Pro (12.9-inch) (2nd generation)", +"iPad Pro (12.9-inch) (3rd generation)" ]) languages([ diff --git a/install_swiftlint.sh b/install_swiftlint.sh index 17720e3..4661c67 100644 --- a/install_swiftlint.sh +++ b/install_swiftlint.sh @@ -7,7 +7,7 @@ set -e SWIFTLINT_PKG_PATH="/tmp/SwiftLint.pkg" -SWIFTLINT_PKG_URL="https://github.com/realm/SwiftLint/releases/download/0.20.1/SwiftLint.pkg" +SWIFTLINT_PKG_URL="https://github.com/realm/SwiftLint/releases/download/0.30.1/SwiftLint.pkg" wget --output-document=$SWIFTLINT_PKG_PATH $SWIFTLINT_PKG_URL