├── .swift-version ├── .swiftformat ├── Cargo.toml ├── Mintfile ├── readme-assets └── screenshot.png ├── swift ├── MinecraftStatus │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 100.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 180.png │ │ │ ├── 20.png │ │ │ ├── 29.png │ │ │ ├── 40.png │ │ │ ├── 50.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 1024.png │ │ │ └── Contents.json │ │ ├── twitterLogoWhite.imageset │ │ │ ├── twitter1x.png │ │ │ ├── twitter2x.png │ │ │ ├── twitter3x.png │ │ │ └── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── twitterBlue.colorset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── MinecraftStatusApp.swift │ ├── Info.plist │ ├── SettingsView.swift │ └── McStructs.swift ├── MinecraftStatusWidgetViewExtension │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── minecraft-dirt.imageset │ │ │ ├── minecraft_dirt.jpg │ │ │ └── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── WidgetBackground.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Fonts │ │ ├── LICENSE │ │ └── minecraft.ttf │ ├── MinecraftStatusWidgetViewExtension.entitlements │ ├── Info.plist │ ├── MinecraftStatusWidgetViewExtension.swift │ └── MinecraftStatusWidgetViewExtension.intentdefinition ├── MinecraftStatusLib │ └── Minecraft-Status-Bridging-Header.h ├── scripts │ ├── swiftlint.sh │ ├── swiftformat.sh │ ├── bootstrap.sh │ └── generate-rust-lib.sh ├── MinecraftStatus.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ ├── Minecraft Status Release.xcscheme │ │ │ ├── Minecraft Status.xcscheme │ │ │ └── McpingWidgetViewExtension.xcscheme │ └── project.pbxproj ├── MinecraftStatus.entitlements ├── MinecraftStatusTests │ ├── Info.plist │ └── MinecraftStatusTests.swift └── MinecraftStatusUITests │ ├── Info.plist │ └── MinecraftStatusUITests.swift ├── rust-toolchain ├── .gitignore ├── .swiftlint.yml ├── PRIVACY_POLICY.md ├── rust ├── src │ ├── bin │ │ └── gen_identicon.rs │ ├── identicon.rs │ ├── tests │ │ └── mod.rs │ ├── mcping_common.rs │ ├── week_stats.rs │ └── lib.rs └── Cargo.toml ├── SETUP.md ├── .github └── workflows │ ├── rust-audit.yml │ ├── rust-ci.yml │ └── swift-ci.yml ├── RELEASE.md ├── LICENSE ├── README.md ├── CHANGELOG.md └── Cargo.lock /.swift-version: -------------------------------------------------------------------------------- 1 | 5.3 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --self insert 2 | --ranges no-space 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "rust" 4 | ] 5 | -------------------------------------------------------------------------------- /Mintfile: -------------------------------------------------------------------------------- 1 | realm/SwiftLint@0.45.0 2 | nicklockwood/SwiftFormat@0.48.17 3 | -------------------------------------------------------------------------------- /readme-assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/readme-assets/screenshot.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.56.0" 3 | components = ["rustfmt", "clippy"] 4 | targets = ["aarch64-apple-ios", "x86_64-apple-ios"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | xcuserdata/ 4 | .DS_Store 5 | swift/MinecraftStatusLib/minecraft_status.h 6 | swift/MinecraftStatusLib/minecraft_status.a 7 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Fonts/LICENSE: -------------------------------------------------------------------------------- 1 | minecraft.ttf was obtained from https://fonts2u.com/minecraft-regular.font and is "free for commerical use" 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_comma 3 | - todo 4 | - line_length 5 | - multiple_closures_with_trailing_closure 6 | identifier_name: 7 | min_length: 1 8 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Fonts/minecraft.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatusWidgetViewExtension/Fonts/minecraft.ttf -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Minecraft Status Privacy Policy 2 | 3 | Minecraft Status operates entirely on-device and sends no information back to the developer or to any third-party services. 4 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /swift/MinecraftStatusLib/Minecraft-Status-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #include "minecraft_status.h" 6 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/twitterLogoWhite.imageset/twitter1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/twitterLogoWhite.imageset/twitter1x.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/twitterLogoWhite.imageset/twitter2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/twitterLogoWhite.imageset/twitter2x.png -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/twitterLogoWhite.imageset/twitter3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatus/Assets.xcassets/twitterLogoWhite.imageset/twitter3x.png -------------------------------------------------------------------------------- /swift/scripts/swiftlint.sh: -------------------------------------------------------------------------------- 1 | if which mint >/dev/null; then 2 | cd $SRCROOT/../ 3 | xcrun --sdk macosx mint run swiftlint 4 | else 5 | echo "warning: Mint not installed, see setup instructions in README" 6 | fi 7 | -------------------------------------------------------------------------------- /swift/scripts/swiftformat.sh: -------------------------------------------------------------------------------- 1 | if which mint >/dev/null; then 2 | cd $SRCROOT/../ 3 | xcrun --sdk macosx mint run swiftformat . 4 | else 5 | echo "warning: Mint not installed, see setup instructions in README" 6 | fi 7 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Assets.xcassets/minecraft-dirt.imageset/minecraft_dirt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cldfire/minecraft-status/HEAD/swift/MinecraftStatusWidgetViewExtension/Assets.xcassets/minecraft-dirt.imageset/minecraft_dirt.jpg -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /swift/scripts/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "checking for homebrew updates"; 4 | brew update 5 | 6 | function install_current { 7 | echo "trying to update $1" 8 | brew upgrade $1 || brew install $1 || true 9 | brew link $1 10 | } 11 | 12 | if [ -e "Mintfile" ]; then 13 | install_current mint 14 | mint bootstrap 15 | fi 16 | -------------------------------------------------------------------------------- /rust/src/bin/gen_identicon.rs: -------------------------------------------------------------------------------- 1 | use minecraft_status::{ 2 | identicon::{self, IdenticonInput}, 3 | mcping_common::ProtocolType, 4 | }; 5 | 6 | fn main() { 7 | let input = IdenticonInput { 8 | protocol_type: ProtocolType::Bedrock, 9 | address: "try.ok.game.org", 10 | }; 11 | println!("{}", identicon::make_base64_identicon(input).unwrap()); 12 | } 13 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.dev.cldfire.minecraft-status 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/MinecraftStatusWidgetViewExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.dev.cldfire.minecraft-status 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | 1. Install [Rust](https://www.rust-lang.org/) 4 | 2. Install [`cbindgen`](https://github.com/eqrion/cbindgen): 5 | 6 | ``` 7 | cargo install cbindgen 8 | ``` 9 | 10 | 3. Install [`homebrew`](https://brew.sh/) 11 | 12 | 4. Run `swift/scripts/bootstrap.sh` 13 | 14 | ``` 15 | sh swift/scripts/bootstrap.sh 16 | ``` 17 | 18 | After performing the above steps, open the XCode project in XCode and build the `Minecraft Status` scheme. 19 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/twitterBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "242", 9 | "green" : "161", 10 | "red" : "29" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Assets.xcassets/minecraft-dirt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "minecraft_dirt.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/twitterLogoWhite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "twitter1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "twitter2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "twitter3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/rust-audit.yml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | - cron: '0 0 * * 0' 4 | push: 5 | paths: 6 | - '**/Cargo.toml' 7 | - '**/Cargo.lock' 8 | branches: 9 | - main 10 | pull_request: 11 | paths: 12 | - '**/Cargo.toml' 13 | - '**/Cargo.lock' 14 | 15 | name: Rust Security audit 16 | 17 | jobs: 18 | audit: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions-rs/audit-check@v1 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minecraft-status" 3 | version = "0.1.0" 4 | authors = ["Cldfire"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["lib", "staticlib"] 11 | 12 | [dependencies] 13 | mcping = { git = "https://github.com/Scetch/mcping.git" } 14 | anyhow = "1.0" 15 | serde = "1.0" 16 | serde_json = "1.0" 17 | expect-test = "1.1" 18 | identicon-rs = "2.0.2" 19 | base64 = "0.13" 20 | image = "0.23.14" 21 | chrono = { version = "0.4", features = ["serde"] } 22 | 23 | [dev-dependencies] 24 | tempfile = "3.2" 25 | 26 | [features] 27 | # enables tests that require an internet connection 28 | online = [] 29 | -------------------------------------------------------------------------------- /swift/MinecraftStatusTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /swift/MinecraftStatusUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | This file documents the release process for Minecraft Status. 4 | 5 | ## TestFlight 6 | 7 | 1. Bump build / version numbers for all targets 8 | 1. Update changelog 9 | 1. Do a `cargo clean` 10 | 1. Switch to the "Minecraft Status" scheme in XCode, building for "Any iOS Device" 11 | 1. Product > Archive from the menubar 12 | 1. Window > Organizer from the menubar 13 | 1. Select the new archive, click "Validate App" and run through that 14 | 1. Select the new archive, click "Distribute App" and upload the archive to the app store 15 | * Make sure to not include bitcode 16 | 1. Wait for the build to appear in App Store Connect 17 | 1. Wait for the build to be processed 18 | 1. Handle the "Manage Compliance" stuff 19 | 1. Add it to the TestFlight group so testers can install it 20 | 1. Commit and tag release (tag format: v0.0.1-3 where 3 is the build number) 21 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/MinecraftStatusApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct MinecraftStatusApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | SettingsView() 10 | } 11 | } 12 | } 13 | 14 | extension MinecraftStatusApp { 15 | /// App's current version. 16 | static var version: String? { 17 | Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String 18 | } 19 | 20 | /// App's current build number. 21 | static var build: String? { 22 | Bundle.main.infoDictionary?["CFBundleVersion"] as? String 23 | } 24 | 25 | /// App's current version and build number. 26 | static var fullVersion: String? { 27 | guard let version = version else { return nil } 28 | guard let build = build else { return version } 29 | return "\(version) (\(build))" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /swift/MinecraftStatusTests/MinecraftStatusTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | @testable import MinecraftStatus 4 | import XCTest 5 | 6 | class MinecraftStatusTests: XCTestCase { 7 | override func setUpWithError() throws { 8 | // Put setup code here. This method is called before the invocation of each test method in the class. 9 | } 10 | 11 | override func tearDownWithError() throws { 12 | // Put teardown code here. This method is called after the invocation of each test method in the class. 13 | } 14 | 15 | func testExample() throws { 16 | // This is an example of a functional test case. 17 | // Use XCTAssert and related functions to verify your tests produce the correct results. 18 | } 19 | 20 | func testPerformanceExample() throws { 21 | // This is an example of a performance test case. 22 | measure { 23 | // Put the code you want to measure the time of here. 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jarek Samic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | MinecraftStatusWidgetViewExtension 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSExtension 24 | 25 | NSExtensionPointIdentifier 26 | com.apple.widgetkit-extension 27 | 28 | UIAppFonts 29 | 30 | minecraft.ttf 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /rust/src/identicon.rs: -------------------------------------------------------------------------------- 1 | use identicon_rs::Identicon; 2 | use image::EncodableLayout; 3 | 4 | use crate::mcping_common::ProtocolType; 5 | 6 | pub struct IdenticonInput<'a> { 7 | pub protocol_type: ProtocolType, 8 | pub address: &'a str, 9 | } 10 | 11 | impl<'a> IdenticonInput<'a> { 12 | fn make_string(&self) -> String { 13 | format!("{:?}{}", self.protocol_type, self.address) 14 | } 15 | } 16 | 17 | pub fn make_base64_identicon(input: IdenticonInput) -> Option { 18 | let identicon = Identicon::new(input.make_string()) 19 | .size(9) 20 | .unwrap() 21 | .scale(54) 22 | .unwrap() 23 | .border(6) 24 | .background_color((0, 0, 0)); 25 | let dynamic_image = identicon.generate_image(); 26 | let mut rgba_image = dynamic_image.to_rgba8(); 27 | 28 | // Replace the background color with transparency 29 | // 30 | // We handle the background in swiftui land so we can react to system theme 31 | // changes 32 | rgba_image 33 | .pixels_mut() 34 | .filter(|p| *p == &image::Rgba([0, 0, 0, 255])) 35 | .for_each(|p| *p = image::Rgba([0, 0, 0, 0])); 36 | 37 | let mut buffer = Vec::new(); 38 | 39 | image::png::PngEncoder::new(&mut buffer) 40 | .encode( 41 | rgba_image.as_bytes(), 42 | rgba_image.width(), 43 | rgba_image.height(), 44 | image::ColorType::Rgba8, 45 | ) 46 | .ok()?; 47 | 48 | Some(base64::encode(&buffer)) 49 | } 50 | -------------------------------------------------------------------------------- /swift/MinecraftStatusUITests/MinecraftStatusUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import XCTest 4 | 5 | class MinecraftStatusUITests: XCTestCase { 6 | override func setUpWithError() throws { 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | 9 | // In UI tests it is usually best to stop immediately when a failure occurs. 10 | continueAfterFailure = false 11 | 12 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 13 | } 14 | 15 | override func tearDownWithError() throws { 16 | // Put teardown code here. This method is called after the invocation of each test method in the class. 17 | } 18 | 19 | func testExample() throws { 20 | // UI tests must launch the application that they test. 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Use recording to get started writing UI tests. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testLaunchPerformance() throws { 29 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 30 | // This measures how long it takes to launch your application. 31 | measure(metrics: [XCTApplicationLaunchMetric()]) { 32 | XCUIApplication().launch() 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/rust-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Rust CI 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | - uses: actions-rs/cargo@v1 20 | with: 21 | command: check 22 | 23 | test: 24 | name: Test 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: stable 32 | - uses: actions-rs/cargo@v1 33 | with: 34 | command: test 35 | 36 | fmt: 37 | name: Rustfmt 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: actions-rs/toolchain@v1 42 | with: 43 | profile: minimal 44 | toolchain: stable 45 | components: rustfmt 46 | - uses: actions-rs/cargo@v1 47 | with: 48 | command: fmt 49 | args: --all -- --check 50 | 51 | clippy: 52 | name: Clippy 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v2 56 | - uses: actions-rs/toolchain@v1 57 | with: 58 | profile: minimal 59 | toolchain: stable 60 | components: clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minecraft-status 2 | 3 | ![Rust CI](https://github.com/Cldfire/minecraft-status/workflows/Rust%20CI/badge.svg) 4 | ![Swift CI](https://github.com/Cldfire/minecraft-status/workflows/Swift%20CI/badge.svg) 5 | [![dependency status](https://deps.rs/repo/github/cldfire/minecraft-status/status.svg)](https://deps.rs/repo/github/cldfire/minecraft-status) 6 | 7 | An iOS app with widgets that display the results of pinging a Minecraft server. Both Bedrock and Java edition servers are supported. 8 | 9 | Built on top of the [mcping](https://github.com/Scetch/mcping) crate. 10 | 11 | ## Note 12 | 13 | **Development of this app has been stopped.** Apple rejected it from the App Store for "not having enough useful content", and I have no desire to bloat the app with unnecessary features beyond being an eye-catching, functional home screen widget for Minecraft servers. Additionally, I would have to pay Apple $99/year for the rest of my life to keep this app available for download, and I have no plans to monetize it in any way to recoup any of that cost. 14 | 15 | Feel free to compile and run for yourself or fork the project. If you decide to fork the project and develop this to a point where you can get it onto the App Store, please take care to remove anything referencing me from the app. (I'd love to hear about it though! 😄.) 16 | 17 | ## Screenshots 18 | 19 | screenshot of small widget on home screen 20 | 21 | ## Features 22 | 23 | * Java and Bedrock support 24 | * Generated identicons for servers without icons 25 | 26 | ## Building 27 | 28 | See [`SETUP.md`](./SETUP.md). 29 | 30 | ## Links 31 | 32 | [Privacy Policy](./PRIVACY_POLICY.md) 33 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Minecraft Status 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSRequiresIPhoneOS 24 | 25 | NSUserActivityTypes 26 | 27 | ConfigurationIntent 28 | SelectServerAddressIntent 29 | 30 | UIApplicationSceneManifest 31 | 32 | UIApplicationSupportsMultipleScenes 33 | 34 | 35 | UIApplicationSupportsIndirectInputEvents 36 | 37 | UILaunchScreen 38 | 39 | UIRequiredDeviceCapabilities 40 | 41 | armv7 42 | 43 | UISupportedInterfaceOrientations 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UISupportedInterfaceOrientations~ipad 50 | 51 | UIInterfaceOrientationPortrait 52 | UIInterfaceOrientationPortraitUpsideDown 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/swift-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Swift CI 8 | 9 | jobs: 10 | Lint: 11 | runs-on: macos-latest 12 | env: 13 | MINT_PATH: ${{ github.workspace }}/mint 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v1 17 | 18 | - name: Cache Mint Packages 19 | uses: actions/cache@v1 20 | with: 21 | path: ${{ env.MINT_PATH }} 22 | key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }} 23 | restore-keys: ${{ runner.os }}-mint- 24 | 25 | - name: Swift Bootstrap 26 | run: sh swift/scripts/bootstrap.sh 27 | 28 | - name: SwiftLint 29 | run: mint run swiftlint 30 | 31 | - name: SwiftFormat 32 | run: mint run swiftformat . --lint 33 | 34 | Test: 35 | runs-on: macos-latest 36 | env: 37 | MINT_PATH: ${{ github.workspace }}/mint 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v1 41 | 42 | - name: Cache Mint Packages 43 | uses: actions/cache@v1 44 | with: 45 | path: ${{ env.MINT_PATH }} 46 | key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }} 47 | restore-keys: ${{ runner.os }}-mint- 48 | 49 | - name: Swift Bootstrap 50 | run: sh swift/scripts/bootstrap.sh 51 | 52 | - name: Rust Toolchain 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | profile: minimal 56 | toolchain: stable 57 | 58 | - name: Install cbindgen 59 | uses: actions-rs/cargo@v1 60 | with: 61 | command: install 62 | args: cbindgen 63 | 64 | - name: Test 65 | uses: sersoft-gmbh/xcodebuild-action@v1 66 | with: 67 | project: swift/MinecraftStatus.xcodeproj 68 | scheme: Minecraft Status 69 | destination: platform=iOS Simulator,name=iPhone 12 70 | action: test 71 | # quiet: false 72 | # use-xcpretty: false 73 | # hide-shell-script-environment: false 74 | 75 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /swift/scripts/generate-rust-lib.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | set -e 3 | # Location of rust project 4 | RUST_PROJ="../rust" 5 | # Location of the lib folder in the XCode project 6 | XCODE_PROJ_LIB="$PROJECT_DIR/MinecraftStatusLib" 7 | # Provide access to Rust utilities 8 | PATH="$HOME/.cargo/bin:$PATH" 9 | 10 | if [ $CONFIGURATION = "Release" ] 11 | then 12 | export RELEASE_OR_DEBUG="release" 13 | else 14 | export RELEASE_OR_DEBUG="debug" 15 | fi 16 | 17 | cd "$RUST_PROJ" 18 | 19 | AARCH64_OBJECT_PATH=../target/aarch64-apple-ios/$RELEASE_OR_DEBUG/libminecraft_status.a 20 | X86_OBJECT_PATH=../target/x86_64-apple-ios/$RELEASE_OR_DEBUG/libminecraft_status.a 21 | UNIVERSAL_OBJECT_PATH=$XCODE_PROJ_LIB/minecraft_status.a 22 | HEADER_FILE_PATH=$XCODE_PROJ_LIB/minecraft_status.h 23 | 24 | if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then 25 | # Assume we're in Xcode, which means we're probably cross-compiling. 26 | # In this case, we need to add an extra library search path for build scripts and proc-macros, 27 | # which run on the host instead of the target. 28 | # (macOS Big Sur does not have linkable libraries in /usr/lib/.) 29 | # 30 | # Workaround taken from https://github.com/signalapp/libsignal-client/blob/master/swift/build_ffi.sh 31 | export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}" 32 | fi 33 | 34 | # Build for iOS architectures 35 | # 36 | # For some reason, having LIBRARY_PATH set when building for x86_64-apple-ios breaks 37 | # the build on my machine, so unsetting it prior to building those targets 38 | if [ $RELEASE_OR_DEBUG = "release" ] 39 | then 40 | cargo build --release --target aarch64-apple-ios 41 | export LIBRARY_PATH="" 42 | cargo build --release --target x86_64-apple-ios 43 | else 44 | cargo build --target aarch64-apple-ios 45 | export LIBRARY_PATH="" 46 | cargo build --target x86_64-apple-ios 47 | fi 48 | 49 | # We only want to perform these actions if cargo had to rebuild something (the file cargo generated has a newer timestamp 50 | # than what we generated the last time cargo rebuilt something) or if the header file doesn't exist 51 | # 52 | # This keeps live-updating previews working in the swiftui code 53 | # 54 | # TODO: if you build for debug, then build for release, then build for debug again, this will not overwrite the release version 55 | if [ $AARCH64_OBJECT_PATH -nt $UNIVERSAL_OBJECT_PATH -o ! -a $HEADER_FILE_PATH ] 56 | then 57 | # Generate C bindings 58 | cbindgen -l C -o $HEADER_FILE_PATH 59 | # Combine object files into a universal library 60 | lipo -create $AARCH64_OBJECT_PATH $X86_OBJECT_PATH -output $UNIVERSAL_OBJECT_PATH 61 | fi 62 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.xcodeproj/xcshareddata/xcschemes/Minecraft Status Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # minecraft-status changelog 2 | 3 | Notable `minecraft-status` changes, tracked in the [keep a changelog](https://keepachangelog.com/en/1.0.0/) format with the addition of the `Internal` change type. 4 | 5 | ## [Unreleased] 6 | 7 | ## [v1.0.0-2] - 2021-10-24 8 | 9 | A quick build with no changes from the previous one to replace the expired 1.0.0-1 build. Apple required some changes I haven't had time to make yet prior to release on the App Store. 10 | 11 | ## [v1.0.0-1] - 2021-07-05 12 | 13 | The 1.0 release that will be going live on the App Store soon! While I was initially planning to add more widget types to the app prior to launching on the App Store, real-life time constraints put an end to those ideas, so I've cleaned up the wonderful existing functionality in the "Minecraft Server Icon" widget and gotten the necessary bits together to release Minecraft Status to the world. 14 | 15 | ### Changed 16 | 17 | * The "Minecraft Server Icon" widget now has an improved appearance in the large size 18 | * The app icon has been updated to be more release-worthy 19 | 20 | ### Fixed 21 | 22 | * "Widget Setup" now links to the iPad widget help article on iPads 23 | 24 | ### Internal 25 | 26 | * Set up a Swift CI test job 27 | * Renamed a bunch of stuff for more consistent naming 28 | * Fixed multiple build issues 29 | * Disabled bitcode in the xcode project settings 30 | 31 | ## [v0.1.0-5] - 2021-04-14 32 | 33 | This release adds support for pinging Bedrock Minecraft servers! The widgets default to an "Auto" mode that can ping any Minecraft server type successfully, and you can choose a specific type in the widget's settings as well if necessary. 34 | 35 | ### Added 36 | 37 | * Added support for pinging Bedrock servers 38 | * Added an auto-detect mode that will successfully ping both Bedrock and Java servers 39 | * Added identicon generation to provide an icon for servers that don't set one 40 | * Bedrock servers can't set icons at all, so this is especially important there 41 | * Added a toggle to choose between displaying the server's favicon or a generated identicon 42 | * Added a link to my Twitter so you can follow me ;) 43 | 44 | ### Internal 45 | 46 | * Moved all of the business logic entirely into Rust 47 | * Created a basic test suite to ensure the core logic doesn't break 48 | 49 | ## [v0.1.0-4] - 2021-02-28 50 | 51 | A small iteration on the first beta release with some quality of life improvements. 52 | 53 | ### Added 54 | 55 | * Server favicons are now cached 56 | * Offline servers are now neatly displayed as being offline in the widget (as long as the server was successfully pinged before) 57 | 58 | ### Changed 59 | 60 | * Latency is no longer considered when choosing a color for the status circle 61 | * It's now either green when the server is online or gray when it's offline 62 | * I personally found using latency as a data point to be more annoying than helpful 63 | * TCP connections are now performed with a five-second timeout 64 | * A widget trying to ping "google.com" will now timeout and display an error message instead of remaining in the redacted view forever 65 | 66 | ### Fixed 67 | 68 | * The app no longer crashes when tapping "Share" on an iPad 69 | 70 | ### Internal 71 | 72 | * Set up CI 73 | * Added build setup guide 74 | * Reorganized the repo 75 | 76 | ## [v0.1.0-3] - 2021-01-28 77 | 78 | Initial release. 79 | 80 | [Unreleased]: https://github.com/Cldfire/minecraft-status/compare/v1.0.0-2...HEAD 81 | [v1.0.0-2]: https://github.com/Cldfire/minecraft-status/compare/v1.0.0-1...v1.0.0-2 82 | [v1.0.0-1]: https://github.com/Cldfire/minecraft-status/compare/v0.0.1-5...v1.0.0-1 83 | [v0.1.0-5]: https://github.com/Cldfire/minecraft-status/compare/v0.0.1-4...v0.0.1-5 84 | [v0.1.0-4]: https://github.com/Cldfire/minecraft-status/compare/v0.0.1-3...v0.0.1-4 85 | [v0.1.0-3]: https://github.com/Cldfire/minecraft-status/releases/tag/v0.0.1-3 86 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.xcodeproj/xcshareddata/xcschemes/Minecraft Status.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.xcodeproj/xcshareddata/xcschemes/McpingWidgetViewExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | 81 | 85 | 86 | 90 | 91 | 95 | 96 | 97 | 98 | 105 | 107 | 113 | 114 | 115 | 116 | 118 | 119 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /rust/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{free_status_response, get_server_status_rust, mcping_common::ProtocolType}; 2 | use expect_test::{expect, Expect}; 3 | use tempfile::tempdir; 4 | 5 | fn check( 6 | server_address: &str, 7 | app_group_container: Option<&str>, 8 | protocol_type: ProtocolType, 9 | always_use_identicon: bool, 10 | expect: Expect, 11 | ) { 12 | let dir = tempdir().unwrap(); 13 | 14 | let app_group_container = app_group_container.unwrap_or_else(|| dir.path().to_str().unwrap()); 15 | 16 | let result = get_server_status_rust( 17 | server_address, 18 | protocol_type, 19 | always_use_identicon, 20 | app_group_container, 21 | ) 22 | // Use display impl since most of the debug values are unstable 23 | .map(|status| { 24 | let string = status.to_string(); 25 | free_status_response(status); 26 | 27 | string 28 | }); 29 | expect.assert_debug_eq(&result); 30 | } 31 | 32 | #[test] 33 | fn blank_server_address() { 34 | check( 35 | "", 36 | None, 37 | ProtocolType::Java, 38 | false, 39 | expect![[r#" 40 | Err( 41 | "empty server address", 42 | ) 43 | "#]], 44 | ); 45 | } 46 | 47 | #[test] 48 | fn blank_app_group_container_path() { 49 | check( 50 | "test", 51 | Some(""), 52 | ProtocolType::Java, 53 | false, 54 | expect![[r#" 55 | Err( 56 | "empty app group container path", 57 | ) 58 | "#]], 59 | ); 60 | } 61 | 62 | #[test] 63 | fn ping_success_basic() { 64 | check( 65 | "test.server.basic", 66 | None, 67 | ProtocolType::Java, 68 | false, 69 | expect![[r#" 70 | Ok( 71 | "Online: McInfoRaw { protocol_type: Java, favicon: \"Generated\" }", 72 | ) 73 | "#]], 74 | ); 75 | } 76 | 77 | #[test] 78 | fn ping_success_full() { 79 | check( 80 | "test.server.full", 81 | None, 82 | ProtocolType::Java, 83 | false, 84 | expect![[r#" 85 | Ok( 86 | "Online: McInfoRaw { protocol_type: Java, favicon: \"ServerProvided\" }", 87 | ) 88 | "#]], 89 | ); 90 | } 91 | 92 | #[test] 93 | fn ping_failure_dnslookupfails() { 94 | check( 95 | "test.server.dnslookupfails", 96 | None, 97 | ProtocolType::Java, 98 | false, 99 | expect![[r#" 100 | Err( 101 | DnsLookupFailed, 102 | ) 103 | "#]], 104 | ); 105 | } 106 | 107 | #[test] 108 | fn always_use_identicon() { 109 | check( 110 | "test.server.full", 111 | None, 112 | ProtocolType::Java, 113 | true, 114 | expect![[r#" 115 | Ok( 116 | "Online: McInfoRaw { protocol_type: Java, favicon: \"Generated\" }", 117 | ) 118 | "#]], 119 | ); 120 | } 121 | 122 | // TODO: tests around file handling, caching 123 | // TODO: tests using the C api 124 | 125 | #[test] 126 | #[cfg(feature = "online")] 127 | fn ping_hypixel() { 128 | check( 129 | "mc.hypixel.net", 130 | None, 131 | ProtocolType::Java, 132 | false, 133 | expect![[r#" 134 | Ok( 135 | "Online: McInfoRaw { protocol_type: Java, favicon: \"ServerProvided\" }", 136 | ) 137 | "#]], 138 | ); 139 | } 140 | 141 | #[test] 142 | #[cfg(feature = "online")] 143 | fn ping_google_lol() { 144 | check( 145 | "google.com", 146 | None, 147 | ProtocolType::Java, 148 | false, 149 | expect![[r#" 150 | Err( 151 | IoError( 152 | Custom { 153 | kind: TimedOut, 154 | error: "connection timed out", 155 | }, 156 | ), 157 | ) 158 | "#]], 159 | ); 160 | } 161 | 162 | #[test] 163 | #[cfg(feature = "online")] 164 | fn ping_hyperlands() { 165 | check( 166 | "play.hyperlandsmc.net:19132", 167 | None, 168 | ProtocolType::Bedrock, 169 | false, 170 | expect![[r#" 171 | Ok( 172 | "Online: McInfoRaw { protocol_type: Bedrock, favicon: \"Generated\" }", 173 | ) 174 | "#]], 175 | ); 176 | } 177 | 178 | #[test] 179 | #[cfg(feature = "online")] 180 | fn ping_hypixel_auto() { 181 | check( 182 | "mc.hypixel.net", 183 | None, 184 | ProtocolType::Auto, 185 | false, 186 | expect![[r#" 187 | Ok( 188 | "Online: McInfoRaw { protocol_type: Java, favicon: \"ServerProvided\" }", 189 | ) 190 | "#]], 191 | ); 192 | } 193 | 194 | #[test] 195 | #[cfg(feature = "online")] 196 | fn ping_hyperlands_auto() { 197 | check( 198 | "play.hyperlandsmc.net", 199 | None, 200 | ProtocolType::Auto, 201 | false, 202 | expect![[r#" 203 | Ok( 204 | "Online: McInfoRaw { protocol_type: Bedrock, favicon: \"Generated\" }", 205 | ) 206 | "#]], 207 | ); 208 | } 209 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // UI design inspired by https://github.com/AnderGoig/github-contributions-ios 2 | 3 | import SwiftUI 4 | 5 | struct SettingsView: View { 6 | var body: some View { 7 | NavigationView { 8 | List { 9 | Section { 10 | ForEach(headerRows) { item in 11 | Button(action: { item.action.performAction() }) { 12 | SettingsRow(item: item) 13 | } 14 | } 15 | } 16 | 17 | Section(footer: footer) { 18 | ForEach(footerRows) { item in 19 | Button(action: { item.action.performAction() }) { 20 | SettingsRow(item: item) 21 | } 22 | } 23 | } 24 | } 25 | .listStyle(InsetGroupedListStyle()) 26 | .navigationBarTitle("Minecraft Status", displayMode: .inline) 27 | } 28 | .navigationViewStyle(StackNavigationViewStyle()) 29 | } 30 | 31 | var footer: some View { 32 | MinecraftStatusApp.fullVersion 33 | .map { Text("version \($0)") } 34 | .textCase(.uppercase) 35 | .foregroundColor(.secondary) 36 | .font(.caption2) 37 | .multilineTextAlignment(.center) 38 | .frame(maxWidth: .infinity, alignment: .center) 39 | .listRowInsets(EdgeInsets(top: 24.0, leading: 0.0, bottom: 24.0, trailing: 0.0)) 40 | } 41 | } 42 | 43 | struct SettingsRow: View { 44 | let item: SettingsRowItem 45 | 46 | var body: some View { 47 | HStack(alignment: .center, spacing: 16.0) { 48 | item.imageName.imageForName() 49 | .resizable() 50 | .aspectRatio(contentMode: .fit) 51 | .frame(width: 17.0, height: 17.0) 52 | .padding(6.0) 53 | .background(item.color) 54 | .foregroundColor(.white) 55 | .clipShape(RoundedRectangle(cornerRadius: 6.0, style: .continuous)) 56 | 57 | VStack(alignment: .leading, spacing: 3.0) { 58 | Text(item.title) 59 | .font(.body) 60 | .foregroundColor(.primary) 61 | .lineLimit(1) 62 | 63 | Text(item.subtitle) 64 | .font(.footnote) 65 | .foregroundColor(.secondary) 66 | .lineLimit(1) 67 | } 68 | } 69 | .padding(.vertical, 5.0) 70 | } 71 | } 72 | 73 | struct ContentView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | SettingsView() 76 | } 77 | } 78 | 79 | enum SettingsRowItemAction { 80 | case openUrl(URL) 81 | case shareUrl(URL) 82 | 83 | func performAction() { 84 | switch self { 85 | case let .openUrl(url): 86 | UIApplication.shared.open(url) 87 | case let .shareUrl(url): 88 | present(UIActivityViewController(activityItems: [url], applicationActivities: nil), animated: true) 89 | } 90 | } 91 | } 92 | 93 | enum ImageName { 94 | case system(String) 95 | case asset(String) 96 | 97 | func imageForName() -> Image { 98 | switch self { 99 | case let .system(name): 100 | return Image(systemName: name) 101 | case let .asset(name): 102 | return Image(name) 103 | } 104 | } 105 | } 106 | 107 | func present(_ viewController: UIViewController, animated: Bool, completion: (() -> Void)? = nil) { 108 | guard var topController = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController else { return } 109 | 110 | while let presentedViewController = topController.presentedViewController { 111 | topController = presentedViewController 112 | } 113 | 114 | // TODO: temporary fix so this doesn't crash on iPads, make this better 115 | let popover = viewController.popoverPresentationController 116 | popover?.sourceView = topController.view 117 | popover?.sourceRect = CGRect(x: 0, y: 0, width: 64, height: 64) 118 | 119 | topController.present(viewController, animated: animated, completion: completion) 120 | } 121 | 122 | func widgetHelpURL() -> URL { 123 | switch UIDevice.current.userInterfaceIdiom { 124 | case .pad: 125 | // Link to iPad widget help article 126 | return URL(string: "https://support.apple.com/en-us/HT211328")! 127 | default: 128 | // Link to iPhone widget help article 129 | return URL(string: "https://support.apple.com/en-us/HT207122")! 130 | } 131 | } 132 | 133 | /// The data for a settings row item. 134 | struct SettingsRowItem: Identifiable { 135 | var id = UUID() 136 | var title: String 137 | var subtitle: String 138 | var imageName: ImageName 139 | var color: Color 140 | var action: SettingsRowItemAction 141 | } 142 | 143 | let headerRows = [ 144 | SettingsRowItem(title: "Widget Setup", subtitle: "Learn how to use widgets", imageName: .system("questionmark"), color: .blue, action: .openUrl(widgetHelpURL())), 145 | ] 146 | 147 | let footerRows = [ 148 | SettingsRowItem(title: "Rate the App", subtitle: "Reviews are greatly appreciated!", imageName: .system("star.fill"), color: .pink, action: .openUrl(URL(string: "itms-apps://apps.apple.com/app/id1549596839?action=write-review")!)), 149 | SettingsRowItem(title: "Share", subtitle: "Tell your friends!", imageName: .system("square.and.arrow.up"), color: .green, action: .shareUrl(URL(string: "https://apps.apple.com/app/id1549596839")!)), 150 | SettingsRowItem(title: "Open Source", subtitle: "Contribute and file issues", imageName: .system("swift"), color: .orange, action: .openUrl(URL(string: "https://github.com/Cldfire/minecraft-status")!)), 151 | SettingsRowItem(title: "@_cldfire", subtitle: "Follow me on Twitter for updates", imageName: .asset("twitterLogoWhite"), color: Color("twitterBlue"), action: .openUrl(URL(string: "https://twitter.com/_cldfire")!)), 152 | ] 153 | -------------------------------------------------------------------------------- /rust/src/mcping_common.rs: -------------------------------------------------------------------------------- 1 | //! Provides a common Rust abstraction over both the Java and Bedrock ping protocols. 2 | //! 3 | //! This abstraction provides the ability to both ping an address with a given 4 | //! protocol and ping an address with both protocols, returning in all cases a 5 | //! unified response type that communicates which protocol was successful. 6 | 7 | use std::{io, sync::mpsc, thread, time::Duration}; 8 | 9 | /// The various protocol types that can be used for a ping. 10 | #[repr(C)] 11 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 12 | pub enum ProtocolType { 13 | /// Ping using the Java protocol only. 14 | Java, 15 | /// Ping using the Bedrock protocol only. 16 | Bedrock, 17 | /// Ping using all protocols, returning the first successful ping result. 18 | Auto, 19 | } 20 | 21 | impl std::fmt::Display for ProtocolType { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self { 24 | ProtocolType::Java => f.write_str("java"), 25 | ProtocolType::Bedrock => f.write_str("bedrock"), 26 | ProtocolType::Auto => f.write_str("auto"), 27 | } 28 | } 29 | } 30 | 31 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 32 | pub struct Response { 33 | pub protocol_type: ProtocolType, 34 | pub latency: u64, 35 | pub version: Version, 36 | pub players: Players, 37 | // TODO: turn this into a rich text type 38 | pub motd: String, 39 | /// The server icon (a Base64-encoded PNG image). 40 | pub favicon: Option, 41 | } 42 | 43 | impl Response { 44 | fn from_java(latency: u64, v: mcping::JavaResponse) -> Self { 45 | Self { 46 | protocol_type: ProtocolType::Java, 47 | latency, 48 | version: Version { 49 | name: v.version.name, 50 | protocol: Some(v.version.protocol), 51 | }, 52 | players: Players { 53 | online: v.players.online, 54 | max: v.players.max, 55 | sample: v 56 | .players 57 | .sample 58 | .into_iter() 59 | .flatten() 60 | .map(|p| Player { 61 | name: p.name, 62 | id: p.id, 63 | }) 64 | .collect(), 65 | }, 66 | motd: v.description.text().to_string(), 67 | favicon: v.favicon, 68 | } 69 | } 70 | 71 | fn from_bedrock(latency: u64, v: mcping::BedrockResponse) -> Self { 72 | Self { 73 | protocol_type: ProtocolType::Bedrock, 74 | latency, 75 | version: Version { 76 | name: v.version_name, 77 | protocol: v.protocol_version, 78 | }, 79 | players: Players { 80 | online: v.players_online.unwrap_or(0), 81 | max: v.players_max.unwrap_or(0), 82 | sample: vec![], 83 | }, 84 | motd: format!( 85 | "motd1: {} motd2: {}", 86 | v.motd_1, 87 | v.motd_2.unwrap_or_default() 88 | ), 89 | favicon: None, 90 | } 91 | } 92 | } 93 | 94 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 95 | pub struct Version { 96 | pub name: String, 97 | pub protocol: Option, 98 | } 99 | 100 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 101 | pub struct Players { 102 | pub online: i64, 103 | pub max: i64, 104 | pub sample: Vec, 105 | } 106 | 107 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 108 | pub struct Player { 109 | pub name: String, 110 | pub id: String, 111 | } 112 | 113 | /// A common `get_status` function that can ping Java or Bedrock (or intelligently 114 | /// try both). 115 | pub fn get_status( 116 | server_address: String, 117 | timeout: Option, 118 | protocol_type: ProtocolType, 119 | ) -> Result { 120 | match protocol_type { 121 | ProtocolType::Java => mcping::get_status(mcping::Java { 122 | server_address, 123 | timeout, 124 | }) 125 | .map(|(latency, response)| Response::from_java(latency, response)), 126 | ProtocolType::Bedrock => mcping::get_status(mcping::Bedrock { 127 | server_address, 128 | timeout, 129 | ..Default::default() 130 | }) 131 | .map(|(latency, response)| Response::from_bedrock(latency, response)), 132 | ProtocolType::Auto => get_status_auto(server_address, timeout), 133 | } 134 | } 135 | 136 | /// Implements trying both protocol pings and returning the first successful result. 137 | fn get_status_auto( 138 | server_address: String, 139 | timeout: Option, 140 | ) -> Result { 141 | enum ResponseType { 142 | Java((u64, mcping::JavaResponse)), 143 | Bedrock((u64, mcping::BedrockResponse)), 144 | } 145 | 146 | let (tx, rx) = mpsc::channel::>(); 147 | 148 | let tx2 = tx.clone(); 149 | let server_address2 = server_address.clone(); 150 | 151 | thread::spawn(move || { 152 | let _ = tx.send( 153 | mcping::get_status(mcping::Java { 154 | server_address, 155 | timeout, 156 | }) 157 | .map(|(latency, response)| ResponseType::Java((latency, response))), 158 | ); 159 | }); 160 | 161 | thread::spawn(move || { 162 | let _ = tx2.send( 163 | mcping::get_status(mcping::Bedrock { 164 | server_address: server_address2, 165 | timeout, 166 | ..Default::default() 167 | }) 168 | .map(|(latency, response)| ResponseType::Bedrock((latency, response))), 169 | ); 170 | }); 171 | 172 | for _ in 0..2 { 173 | // Return the first successful response, if any 174 | if let Ok(Ok(response_type)) = rx.recv() { 175 | return Ok(match response_type { 176 | ResponseType::Java((latency, response)) => Response::from_java(latency, response), 177 | ResponseType::Bedrock((latency, response)) => { 178 | Response::from_bedrock(latency, response) 179 | } 180 | }); 181 | } 182 | } 183 | 184 | Err(mcping::Error::IoError(io::Error::new( 185 | io::ErrorKind::TimedOut, 186 | "neither thread returned a valid response", 187 | ))) 188 | } 189 | -------------------------------------------------------------------------------- /swift/MinecraftStatus/McStructs.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | let sharedContainer: URL = { 7 | // Write to shared app group container so both the widget and the host app can access 8 | FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.dev.cldfire.minecraft-status")! 9 | }() 10 | 11 | /// Represents the server status we were able to determine 12 | enum ServerStatus { 13 | /// The server was online, and we got the given info 14 | case online(OnlineResponse) 15 | /// The server was unreachable, but we were able to reach it at some point in the past. 16 | /// 17 | /// We may have a cached favicon to make use of. 18 | case offline(OfflineResponse) 19 | /// The server was unreachable, and we've never reached it in the past. 20 | case unreachable(UnreachableResponse) 21 | 22 | /// Attempt to ping the server at the given address. 23 | static func forServerAddress(_ serverAddress: String, _ protocolType: ProtocolType, _ alwaysUseIdenticon: Bool) -> Self { 24 | let status = get_server_status(serverAddress, protocolType, alwaysUseIdenticon, sharedContainer.path) 25 | 26 | defer { 27 | free_status_response(status) 28 | } 29 | 30 | switch status.tag { 31 | case Online: 32 | return .online(OnlineResponse(mcInfo: McInfo(status.online.mcinfo), weekStats: WeekStatsSwift(status.online.week_stats))) 33 | case Offline: 34 | return .offline(OfflineResponse(favicon: Favicon.fromRaw(status.offline.favicon), weekStats: WeekStatsSwift(status.offline.week_stats))) 35 | case Unreachable: 36 | let errorString: String 37 | if let errorStringCstr = status.unreachable.error_string { 38 | errorString = String(cString: errorStringCstr) 39 | } else { 40 | errorString = "" 41 | } 42 | 43 | return .unreachable(UnreachableResponse(errorString: errorString)) 44 | default: 45 | fatalError("unexpected type of server status response") 46 | } 47 | } 48 | 49 | func favicon() -> Favicon { 50 | switch self { 51 | case let .online(response): 52 | return response.mcInfo.favicon 53 | case let .offline(response): 54 | return response.favicon 55 | case .unreachable: 56 | return .noFavicon 57 | } 58 | } 59 | 60 | // We return a Text view here so the resulting string has separators in the numbers 61 | func playersOnlineText() -> Text { 62 | switch self { 63 | case let .online(response): 64 | return Text("\(response.mcInfo.players.online) / \(response.mcInfo.players.max)") 65 | case .offline: 66 | return Text("-- / --") 67 | case .unreachable: 68 | return Text("") 69 | } 70 | } 71 | 72 | func statusColor() -> Color { 73 | if case .online = self { 74 | // We always return green if the server is online, regardless of latency. 75 | // 76 | // This decision was made because latency is oftentimes irregular in the context 77 | // of a phone widget; for instance, when using data rather than wifi. This 78 | // irregularity makes latency a poor data point to use to change the status color, 79 | // and I personally found it more annoying than useful. 80 | return Color.green 81 | } else { 82 | return Color.gray 83 | } 84 | } 85 | } 86 | 87 | struct OnlineResponse { 88 | var mcInfo: McInfo 89 | var weekStats: WeekStatsSwift 90 | } 91 | 92 | struct OfflineResponse { 93 | /// The server icon (a Base64-encoded PNG image) 94 | var favicon: Favicon 95 | var weekStats: WeekStatsSwift 96 | } 97 | 98 | struct UnreachableResponse { 99 | var errorString: String 100 | } 101 | 102 | struct McInfo { 103 | /// Latency to the server 104 | var latency: UInt64 105 | var version: Version 106 | /// Information about online players 107 | var players: Players 108 | /// The server's description text 109 | var description: String 110 | /// The server icon (a Base64-encoded PNG image) 111 | var favicon: Favicon 112 | } 113 | 114 | // Using a separate extension for this initializer keeps the default memberwise initializer 115 | // around 116 | extension McInfo { 117 | /// Copies data from the given `McInfoRaw` in order to create this struct. 118 | init(_ from: McInfoRaw) { 119 | self.latency = from.latency 120 | 121 | self.version = Version(from.version) 122 | self.players = Players(from.players) 123 | 124 | if let descriptionCstr = from.description { 125 | self.description = String(cString: descriptionCstr) 126 | } else { 127 | self.description = "" 128 | } 129 | 130 | self.favicon = Favicon.fromRaw(from.favicon) 131 | } 132 | } 133 | 134 | struct Version { 135 | var name: String 136 | var protocolVersion: Int64 137 | } 138 | 139 | extension Version { 140 | /// Copies data from the given `VersionRaw` in order to create this struct. 141 | init(_ from: VersionRaw) { 142 | if let nameCstr = from.name { 143 | self.name = String(cString: nameCstr) 144 | } else { 145 | self.name = "" 146 | } 147 | 148 | self.protocolVersion = from.protocol 149 | } 150 | } 151 | 152 | struct Player { 153 | var name: String 154 | var id: String 155 | } 156 | 157 | extension Player { 158 | /// Copies data from the given `PlayerRaw` in order to create this struct. 159 | init(_ from: PlayerRaw) { 160 | if let nameCstr = from.name { 161 | self.name = String(cString: nameCstr) 162 | } else { 163 | self.name = "" 164 | } 165 | 166 | if let idCstr = from.id { 167 | self.id = String(cString: idCstr) 168 | } else { 169 | self.id = "" 170 | } 171 | } 172 | } 173 | 174 | struct Players { 175 | var max: Int64 176 | var online: Int64 177 | var sample: [Player] 178 | } 179 | 180 | extension Players { 181 | /// Copies data from the given `PlayersRaw` in order to create this struct. 182 | init(_ from: PlayersRaw) { 183 | self.max = from.max 184 | self.online = from.online 185 | 186 | self.sample = [Player]() 187 | self.sample.reserveCapacity(Int(from.sample_len)) 188 | 189 | for i in 0.. Self { 205 | switch from.tag { 206 | case ServerProvided: 207 | let favicon: String? 208 | if let faviconCstr = from.server_provided { 209 | favicon = String(cString: faviconCstr) 210 | } else { 211 | favicon = nil 212 | } 213 | return .serverProvided(favicon!) 214 | case Generated: 215 | let favicon: String? 216 | if let faviconCstr = from.generated { 217 | favicon = String(cString: faviconCstr) 218 | } else { 219 | favicon = nil 220 | } 221 | return .generated(favicon!) 222 | case NoFavicon: 223 | return noFavicon 224 | default: 225 | fatalError("unexpected type of favicon") 226 | } 227 | } 228 | 229 | func faviconString() -> String? { 230 | switch self { 231 | case let .serverProvided(faviconString): 232 | return faviconString 233 | case let .generated(faviconString): 234 | return faviconString 235 | case .noFavicon: 236 | return nil 237 | } 238 | } 239 | 240 | func isGenerated() -> Bool { 241 | switch self { 242 | case .generated: 243 | return true 244 | default: 245 | return false 246 | } 247 | } 248 | } 249 | 250 | /// Week stats but with an array instead of a tuple. 251 | struct WeekStatsSwift { 252 | var dailyStats: [RangeStats] 253 | var peakOnline: Int64 254 | var peakMax: Int64 255 | } 256 | 257 | extension WeekStatsSwift { 258 | /// Copies data from the given `WeekStats` in order to create this struct. 259 | init(_ from: WeekStats) { 260 | self.dailyStats = [from.daily_stats.0, from.daily_stats.1, from.daily_stats.2, from.daily_stats.3, from.daily_stats.4, from.daily_stats.5, from.daily_stats.6, from.daily_stats.7] 261 | self.peakOnline = from.peak_online 262 | self.peakMax = from.peak_max 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /swift/MinecraftStatusWidgetViewExtension/MinecraftStatusWidgetViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Intents 4 | import SwiftUI 5 | import WidgetKit 6 | 7 | struct Provider: IntentTimelineProvider { 8 | func placeholder(in _: Context) -> McServerStatusEntry { 9 | previewData[0] 10 | } 11 | 12 | func getSnapshot(for _: ConfigurationIntent, in _: Context, completion: @escaping (McServerStatusEntry) -> Void) { 13 | // Preview uses a ping response from mc.hypixel.net 14 | let entry = previewData[2] 15 | completion(entry) 16 | } 17 | 18 | func getTimeline(for configuration: ConfigurationIntent, in _: Context, completion: @escaping (Timeline) -> Void) { 19 | let currentDate = Date() 20 | let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! 21 | let protocolType = protocolTypeFromConfig(configuration.protocolTypeConfig) 22 | let alwaysUseIdenticon = !(configuration.useServerFavicon as? Bool ?? true) 23 | 24 | McPinger.ping(configuration.serverAddress ?? "", protocolType, alwaysUseIdenticon) { serverStatus in 25 | let entry = McServerStatusEntry(date: currentDate, configuration: configuration, status: serverStatus) 26 | let timeline = Timeline(entries: [entry], policy: .after(refreshDate)) 27 | completion(timeline) 28 | } 29 | } 30 | } 31 | 32 | struct McServerStatusEntry: TimelineEntry { 33 | let date: Date 34 | let configuration: ConfigurationIntent 35 | let status: ServerStatus 36 | // TODO: relevance 37 | } 38 | 39 | enum McPinger { 40 | static func ping(_ serverAddress: String, _ protocolType: ProtocolType, _ alwaysUseIdenticon: Bool, completion: @escaping (ServerStatus) -> Void) { 41 | // TODO: not sure that this is the correct way of making a backgroud network request 42 | // in a widget 43 | DispatchQueue.global(qos: .background).async { 44 | let status = ServerStatus.forServerAddress(serverAddress, protocolType, alwaysUseIdenticon) 45 | completion(status) 46 | } 47 | } 48 | } 49 | 50 | func protocolTypeFromConfig(_ protocolTypeConfig: ProtocolTypeConfig) -> ProtocolType { 51 | switch protocolTypeConfig { 52 | case .auto: 53 | return Auto 54 | case .java: 55 | return Java 56 | case .bedrock: 57 | return Bedrock 58 | case .unknown: 59 | return Auto 60 | } 61 | } 62 | 63 | /// Attempts to convert a maybe-present base64 image string into a `UIImage`. 64 | func convertBase64StringToImage(imageBase64String: String?) -> UIImage? { 65 | guard let imageString = imageBase64String else { 66 | return nil 67 | } 68 | 69 | guard let imageData = Data(base64Encoded: imageString) else { 70 | return nil 71 | } 72 | 73 | return UIImage(data: imageData) 74 | } 75 | 76 | func chooseBestHeaderText(for configuration: ConfigurationIntent) -> String { 77 | if let serverName = configuration.serverName, !serverName.isEmpty { 78 | return serverName 79 | } else { 80 | return configuration.serverAddress ?? "" 81 | } 82 | } 83 | 84 | struct ServerFavicon: View { 85 | let favicon: Favicon 86 | @Environment(\.colorScheme) var colorScheme 87 | 88 | var body: some View { 89 | if favicon.isGenerated() { 90 | // Add a background behind generated favicons 91 | Rectangle().foregroundColor(Color(UIColor.systemBackground)) 92 | } else { 93 | // Minecraft dirt block backing 94 | Image("minecraft-dirt").interpolation(.none).antialiased(false).resizable().aspectRatio(contentMode: .fill).unredacted() 95 | Rectangle().opacity(0.60).foregroundColor(.black) 96 | } 97 | 98 | if let faviconImage = convertBase64StringToImage(imageBase64String: favicon.faviconString()) { 99 | Image(uiImage: faviconImage).interpolation(.none).antialiased(false).resizable().aspectRatio(contentMode: .fit).shadow(radius: favicon.isGenerated() ? 0 : 30).colorScheme(.light) 100 | } 101 | } 102 | } 103 | 104 | struct OverlayBanner: View { 105 | var entry: Provider.Entry 106 | @Environment(\.widgetFamily) var family 107 | 108 | var body: some View { 109 | VStack { 110 | // These spacers move the banner towards the bottom of the widget 111 | Spacer() 112 | Spacer() 113 | Spacer() 114 | Spacer() 115 | Spacer() 116 | 117 | HStack { 118 | // The banner content 119 | VStack(alignment: .leading, spacing: 4) { 120 | // Server address 121 | Text("\(chooseBestHeaderText(for: entry.configuration))").foregroundColor(.white).shadow(color: .black, radius: 0.5, x: 1, y: 1).lineLimit(1) 122 | 123 | // Players online and status indicator 124 | HStack(spacing: 5) { 125 | entry.status.playersOnlineText().foregroundColor(.white).shadow(color: .black, radius: 0.5, x: 1, y: 1).minimumScaleFactor(0.8).lineLimit(1) 126 | 127 | Circle().foregroundColor(entry.status.statusColor()).fixedSize().scaleEffect(family == .systemLarge ? 1.0 : 0.9) 128 | } 129 | } 130 | .font(.custom("minecraft", size: family == .systemLarge ? 14 : 12)) 131 | .frame(height: 45, alignment: .leading) 132 | .frame(maxWidth: family == .systemLarge ? .none : .infinity) 133 | .padding(.leading, family == .systemLarge ? 20 : 10) 134 | .padding(.trailing, family == .systemLarge ? 20 : 10) 135 | .padding(.top, family == .systemLarge ? 5 : 0) 136 | .padding(.bottom, family == .systemLarge ? 5 : 0) 137 | .background( 138 | Color.black 139 | .opacity(0.7) 140 | .cornerRadius(family == .systemLarge ? 8 : 0) 141 | ) 142 | 143 | if family == .systemLarge { 144 | // Shove the banner to the left side for the large widget 145 | Spacer() 146 | } 147 | } 148 | .padding(.leading, family == .systemLarge ? 20 : 0) 149 | .padding(.trailing, family == .systemLarge ? 100 : 0) 150 | 151 | // This spacer separates the banner from the bottom of the widget 152 | Spacer() 153 | } 154 | } 155 | } 156 | 157 | struct MinecraftStatusWidgetExtensionEntryView: View { 158 | var entry: Provider.Entry 159 | @Environment(\.widgetFamily) var family 160 | 161 | var body: some View { 162 | if case .unreachable = entry.status { 163 | if let serverAddress = entry.configuration.serverAddress, !serverAddress.isEmpty { 164 | Text("Unable to ping Minecraft server \"\(serverAddress)\"").padding() 165 | } else { 166 | Text("Please edit the widget and set a server address").padding() 167 | } 168 | } else { 169 | ZStack { 170 | ServerFavicon(favicon: entry.status.favicon()) 171 | OverlayBanner(entry: entry) 172 | } 173 | } 174 | } 175 | } 176 | 177 | @main 178 | struct MinecraftStatusWidgetExtension: Widget { 179 | let kind: String = "MinecraftStatusWidgetViewExtension" 180 | 181 | var body: some WidgetConfiguration { 182 | IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in 183 | MinecraftStatusWidgetExtensionEntryView(entry: entry) 184 | } 185 | .configurationDisplayName("Minecraft Server Icon") 186 | .description("Information about a Minecraft server on top of its icon") 187 | // TODO: work on the large widget a bit 188 | .supportedFamilies([.systemSmall, .systemLarge]) 189 | } 190 | } 191 | 192 | struct MinecraftStatusWidgetExtension_Previews: PreviewProvider { 193 | static var previews: some View { 194 | Group { 195 | ForEach(0.. 2 | 3 | 4 | 5 | INEnums 6 | 7 | 8 | INEnumDisplayName 9 | Server Type 10 | INEnumDisplayNameID 11 | GwwkNo 12 | INEnumGeneratesHeader 13 | 14 | INEnumName 15 | ProtocolTypeConfig 16 | INEnumType 17 | Regular 18 | INEnumValues 19 | 20 | 21 | INEnumValueDisplayName 22 | unknown 23 | INEnumValueDisplayNameID 24 | RjxXwA 25 | INEnumValueName 26 | unknown 27 | 28 | 29 | INEnumValueDisplayName 30 | Auto 31 | INEnumValueDisplayNameID 32 | b2e3oo 33 | INEnumValueIndex 34 | 1 35 | INEnumValueName 36 | auto 37 | 38 | 39 | INEnumValueDisplayName 40 | Java 41 | INEnumValueDisplayNameID 42 | ccO2Yy 43 | INEnumValueIndex 44 | 2 45 | INEnumValueName 46 | java 47 | 48 | 49 | INEnumValueDisplayName 50 | Bedrock 51 | INEnumValueDisplayNameID 52 | igreet 53 | INEnumValueIndex 54 | 3 55 | INEnumValueName 56 | bedrock 57 | 58 | 59 | 60 | 61 | INIntentDefinitionModelVersion 62 | 1.2 63 | INIntentDefinitionNamespace 64 | 88xZPY 65 | INIntentDefinitionSystemVersion 66 | 20D91 67 | INIntentDefinitionToolsBuildVersion 68 | 12D4e 69 | INIntentDefinitionToolsVersion 70 | 12.4 71 | INIntents 72 | 73 | 74 | INIntentCategory 75 | information 76 | INIntentDescription 77 | Minecraft Status Widget configuration parameters 78 | INIntentDescriptionID 79 | tVvJ9c 80 | INIntentEligibleForWidgets 81 | 82 | INIntentIneligibleForSuggestions 83 | 84 | INIntentLastParameterTag 85 | 6 86 | INIntentName 87 | Configuration 88 | INIntentParameters 89 | 90 | 91 | INIntentParameterConfigurable 92 | 93 | INIntentParameterDisplayName 94 | Server Address 95 | INIntentParameterDisplayNameID 96 | hWx9l3 97 | INIntentParameterDisplayPriority 98 | 1 99 | INIntentParameterMetadata 100 | 101 | INIntentParameterMetadataCapitalization 102 | None 103 | INIntentParameterMetadataDefaultValue 104 | mc.hypixel.net 105 | INIntentParameterMetadataDefaultValueID 106 | MEFOca 107 | INIntentParameterMetadataDisableAutocorrect 108 | 109 | INIntentParameterMetadataDisableSmartDashes 110 | 111 | INIntentParameterMetadataDisableSmartQuotes 112 | 113 | 114 | INIntentParameterName 115 | serverAddress 116 | INIntentParameterPromptDialogs 117 | 118 | 119 | INIntentParameterPromptDialogCustom 120 | 121 | INIntentParameterPromptDialogType 122 | Configuration 123 | 124 | 125 | INIntentParameterPromptDialogCustom 126 | 127 | INIntentParameterPromptDialogType 128 | Primary 129 | 130 | 131 | INIntentParameterTag 132 | 1 133 | INIntentParameterType 134 | String 135 | 136 | 137 | INIntentParameterConfigurable 138 | 139 | INIntentParameterDisplayName 140 | Server Name 141 | INIntentParameterDisplayNameID 142 | 0zurXT 143 | INIntentParameterDisplayPriority 144 | 2 145 | INIntentParameterMetadata 146 | 147 | INIntentParameterMetadataCapitalization 148 | Sentences 149 | INIntentParameterMetadataDefaultValueID 150 | Emhv7x 151 | INIntentParameterMetadataDisableAutocorrect 152 | 153 | INIntentParameterMetadataDisableSmartDashes 154 | 155 | INIntentParameterMetadataDisableSmartQuotes 156 | 157 | 158 | INIntentParameterName 159 | serverName 160 | INIntentParameterPromptDialogs 161 | 162 | 163 | INIntentParameterPromptDialogCustom 164 | 165 | INIntentParameterPromptDialogType 166 | Configuration 167 | 168 | 169 | INIntentParameterPromptDialogCustom 170 | 171 | INIntentParameterPromptDialogType 172 | Primary 173 | 174 | 175 | INIntentParameterTag 176 | 2 177 | INIntentParameterType 178 | String 179 | 180 | 181 | INIntentParameterConfigurable 182 | 183 | INIntentParameterDisplayName 184 | Server Type 185 | INIntentParameterDisplayNameID 186 | 215ixD 187 | INIntentParameterDisplayPriority 188 | 3 189 | INIntentParameterEnumType 190 | ProtocolTypeConfig 191 | INIntentParameterEnumTypeNamespace 192 | 88xZPY 193 | INIntentParameterMetadata 194 | 195 | INIntentParameterMetadataDefaultValue 196 | auto 197 | 198 | INIntentParameterName 199 | protocolTypeConfig 200 | INIntentParameterPromptDialogs 201 | 202 | 203 | INIntentParameterPromptDialogCustom 204 | 205 | INIntentParameterPromptDialogType 206 | Configuration 207 | 208 | 209 | INIntentParameterPromptDialogCustom 210 | 211 | INIntentParameterPromptDialogType 212 | Primary 213 | 214 | 215 | INIntentParameterPromptDialogCustom 216 | 217 | INIntentParameterPromptDialogFormatString 218 | There are ${count} options matching ‘${protocolTypeConfig}’. 219 | INIntentParameterPromptDialogFormatStringID 220 | QAt7pk 221 | INIntentParameterPromptDialogType 222 | DisambiguationIntroduction 223 | 224 | 225 | INIntentParameterPromptDialogCustom 226 | 227 | INIntentParameterPromptDialogFormatString 228 | Just to confirm, you wanted ‘${protocolTypeConfig}’? 229 | INIntentParameterPromptDialogFormatStringID 230 | JH7A5L 231 | INIntentParameterPromptDialogType 232 | Confirmation 233 | 234 | 235 | INIntentParameterTag 236 | 4 237 | INIntentParameterType 238 | Integer 239 | 240 | 241 | INIntentParameterConfigurable 242 | 243 | INIntentParameterDisplayName 244 | Use Server Favicon 245 | INIntentParameterDisplayNameID 246 | NOA5RA 247 | INIntentParameterDisplayPriority 248 | 4 249 | INIntentParameterMetadata 250 | 251 | INIntentParameterMetadataDefaultValue 252 | 253 | INIntentParameterMetadataFalseDisplayName 254 | false 255 | INIntentParameterMetadataFalseDisplayNameID 256 | 7NKgoi 257 | INIntentParameterMetadataTrueDisplayName 258 | true 259 | INIntentParameterMetadataTrueDisplayNameID 260 | Ov5gzh 261 | 262 | INIntentParameterName 263 | useServerFavicon 264 | INIntentParameterPromptDialogs 265 | 266 | 267 | INIntentParameterPromptDialogCustom 268 | 269 | INIntentParameterPromptDialogType 270 | Configuration 271 | 272 | 273 | INIntentParameterPromptDialogCustom 274 | 275 | INIntentParameterPromptDialogType 276 | Primary 277 | 278 | 279 | INIntentParameterTag 280 | 6 281 | INIntentParameterType 282 | Boolean 283 | 284 | 285 | INIntentResponse 286 | 287 | INIntentResponseCodes 288 | 289 | 290 | INIntentResponseCodeName 291 | success 292 | INIntentResponseCodeSuccess 293 | 294 | 295 | 296 | INIntentResponseCodeName 297 | failure 298 | 299 | 300 | 301 | INIntentTitle 302 | Configuration 303 | INIntentTitleID 304 | gpCwrM 305 | INIntentType 306 | Custom 307 | INIntentVerb 308 | View 309 | 310 | 311 | INTypes 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /rust/src/week_stats.rs: -------------------------------------------------------------------------------- 1 | //! Implements the week stats backend. 2 | //! 3 | //! Collects, stores, and hands out ping stats about a Minecraft server over the 4 | //! last week or so. 5 | 6 | use std::{collections::BTreeMap, fs, ops::RangeBounds, path::Path}; 7 | 8 | use anyhow::Context; 9 | use chrono::{DateTime, Duration, Local, Timelike, Utc}; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[derive(Serialize, Deserialize, Default)] 13 | struct PingStatsOnDisk { 14 | /// History entries keyed by unix timestamp. 15 | ping_history: BTreeMap, 16 | } 17 | 18 | impl PingStatsOnDisk { 19 | /// Trim outdated entries from the beginning of the stored ping history. 20 | /// 21 | /// An entry older than 10 days ago is considered to be outdated. 22 | pub fn trim_outdated(&mut self, now: DateTime) { 23 | let cutoff = now - Duration::days(10); 24 | let cutoff_timestamp = cutoff.timestamp(); 25 | 26 | // TODO: use BTreeMap::retain when it's stable 27 | let remaining = self.ping_history.split_off(&cutoff_timestamp); 28 | self.ping_history = remaining; 29 | } 30 | 31 | /// Incorporate the given ping data appropriately into the stored entries. 32 | pub fn add_data(&mut self, now: DateTime, current_online: i64, current_max: i64) { 33 | self.ping_history 34 | .entry(now.timestamp()) 35 | .or_default() 36 | .update(current_online, current_max); 37 | } 38 | 39 | /// Return `RangeStats` built from data within the given timestamp range. 40 | pub fn range_stats(&self, timestamp_range: impl RangeBounds) -> RangeStats { 41 | let mut num_entries = 0; 42 | let mut total_online = 0; 43 | let mut peak_online = 0; 44 | let mut peak_max = 0; 45 | 46 | for (_, v) in self.ping_history.range(timestamp_range) { 47 | num_entries += 1; 48 | total_online += v.online; 49 | 50 | peak_online = peak_online.max(v.online); 51 | peak_max = peak_max.max(v.max); 52 | } 53 | 54 | RangeStats { 55 | average_online: if num_entries == 0 { 56 | 0 57 | } else { 58 | total_online / num_entries 59 | }, 60 | peak_online, 61 | peak_max, 62 | } 63 | } 64 | 65 | /// Build `WeekStats` from the current state of the data. 66 | pub fn week_stats(&self, now_timestamp: i64, seconds_from_midnight: i64) -> WeekStats { 67 | let today_midnight = now_timestamp - seconds_from_midnight; 68 | 69 | /// Returns the number of seconds in `n` days. 70 | fn days(n: i64) -> i64 { 71 | 60 * 60 * 24 * n 72 | } 73 | 74 | let daily_stats = [ 75 | self.range_stats((today_midnight - days(7))..(today_midnight - days(6))), 76 | self.range_stats((today_midnight - days(6))..(today_midnight - days(5))), 77 | self.range_stats((today_midnight - days(5))..(today_midnight - days(4))), 78 | self.range_stats((today_midnight - days(4))..(today_midnight - days(3))), 79 | self.range_stats((today_midnight - days(3))..(today_midnight - days(2))), 80 | self.range_stats((today_midnight - days(2))..(today_midnight - days(1))), 81 | self.range_stats((today_midnight - days(1))..today_midnight), 82 | self.range_stats(today_midnight..=now_timestamp), 83 | ]; 84 | 85 | let peak_online = daily_stats 86 | .iter() 87 | .map(|s| s.peak_online) 88 | .max() 89 | .unwrap_or_default(); 90 | 91 | let peak_max = daily_stats 92 | .iter() 93 | .map(|s| s.peak_max) 94 | .max() 95 | .unwrap_or_default(); 96 | 97 | WeekStats { 98 | daily_stats, 99 | peak_online, 100 | peak_max, 101 | } 102 | } 103 | } 104 | 105 | /// A ping history entry. 106 | #[derive(Serialize, Deserialize, Default, Clone)] 107 | struct HistoryEntry { 108 | /// The number of players online at this time. 109 | pub online: i64, 110 | /// The max number of players allowed online at this time. 111 | pub max: i64, 112 | } 113 | 114 | impl HistoryEntry { 115 | /// Update this history entry with new data. 116 | fn update(&mut self, current_online: i64, current_max: i64) { 117 | self.online = current_online; 118 | self.max = current_max; 119 | } 120 | } 121 | 122 | /// Stats representing some range of time. 123 | #[repr(C)] 124 | #[derive(Default, Debug, Eq, PartialEq)] 125 | pub struct RangeStats { 126 | /// The average number of players online during this period. 127 | pub average_online: i64, 128 | /// The peak number of online players during this period. 129 | pub peak_online: i64, 130 | /// The peak max allowed online players during this period. 131 | pub peak_max: i64, 132 | } 133 | 134 | #[repr(C)] 135 | #[derive(Debug, Default)] 136 | pub struct WeekStats { 137 | /// Stats for the last eight days. 138 | pub daily_stats: [RangeStats; 8], 139 | /// The peak number of online players during this period. 140 | pub peak_online: i64, 141 | /// The peak max allowed online players during this period. 142 | pub peak_max: i64, 143 | } 144 | 145 | pub fn determine_week_stats( 146 | path: impl AsRef, 147 | current_online: i64, 148 | current_max: i64, 149 | ) -> Result { 150 | let path = path.as_ref(); 151 | 152 | let now_local = Local::now(); 153 | let now_utc = Utc::now(); 154 | 155 | let mut data = if path.exists() { 156 | let data = fs::read(path) 157 | .with_context(|| format!("failed to read week stats file from {}", path.display()))?; 158 | // If parsing fails, we start fresh 159 | serde_json::from_slice(&data).unwrap_or_default() 160 | } else { 161 | PingStatsOnDisk::default() 162 | }; 163 | 164 | data.trim_outdated(now_utc); 165 | data.add_data(now_utc, current_online, current_max); 166 | 167 | let week_stats = data.week_stats( 168 | now_local.timestamp(), 169 | now_local.num_seconds_from_midnight() as i64, 170 | ); 171 | 172 | let updated_data = 173 | serde_json::to_string(&data).with_context(|| "failed to serialize week stats")?; 174 | fs::write(&path, &updated_data) 175 | .with_context(|| format!("failed to write week stats file to {}", path.display()))?; 176 | 177 | Ok(week_stats) 178 | } 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | use chrono::TimeZone; 183 | use tempfile::TempDir; 184 | 185 | use super::*; 186 | 187 | fn moment_utc() -> DateTime { 188 | Utc.ymd(2021, 2, 14).and_hms(8, 12, 43) 189 | } 190 | 191 | fn test_data() -> PingStatsOnDisk { 192 | let mut data = PingStatsOnDisk::default(); 193 | let moment = moment_utc(); 194 | 195 | data.add_data(moment - Duration::days(12) - Duration::hours(3), 20, 70); 196 | data.add_data(moment - Duration::days(10) - Duration::hours(3), 20, 70); 197 | data.add_data(moment - Duration::days(10) + Duration::hours(4), 20, 40); 198 | data.add_data(moment - Duration::days(9), 20, 40); 199 | 200 | data.add_data(moment - Duration::days(6) - Duration::minutes(12), 13, 40); 201 | data.add_data(moment - Duration::days(6) + Duration::hours(5), 40, 40); 202 | 203 | data.add_data(moment - Duration::days(1) - Duration::hours(1), 4, 30); 204 | data.add_data(moment - Duration::days(1) - Duration::minutes(30), 3, 50); 205 | data.add_data(moment - Duration::days(1), 20, 30); 206 | 207 | data.add_data(moment - Duration::hours(2), 15, 30); 208 | data.add_data(moment - Duration::minutes(15), 5, 30); 209 | data.add_data(moment, 10, 30); 210 | 211 | data 212 | } 213 | 214 | #[test] 215 | fn trim_outdated() { 216 | let mut data = test_data(); 217 | let original_length = data.ping_history.len(); 218 | let moment = moment_utc(); 219 | 220 | data.trim_outdated(moment); 221 | 222 | assert!(data.ping_history.len() < original_length); 223 | 224 | // These entries were outdated and should have been trimmed 225 | assert_eq!( 226 | data.ping_history 227 | .contains_key(&(moment - Duration::days(12) - Duration::hours(3)).timestamp()), 228 | false 229 | ); 230 | assert_eq!( 231 | data.ping_history 232 | .contains_key(&(moment - Duration::days(10) - Duration::hours(3)).timestamp()), 233 | false 234 | ); 235 | 236 | // These entries should have been kept 237 | assert_eq!( 238 | data.ping_history 239 | .contains_key(&(moment - Duration::days(10) + Duration::hours(4)).timestamp()), 240 | true 241 | ); 242 | assert_eq!( 243 | data.ping_history 244 | .contains_key(&(moment - Duration::days(1)).timestamp()), 245 | true 246 | ); 247 | assert_eq!(data.ping_history.contains_key(&moment.timestamp()), true); 248 | } 249 | 250 | #[test] 251 | fn week_stats() { 252 | let data = test_data(); 253 | let moment = moment_utc(); 254 | 255 | let week_stats = data.week_stats( 256 | moment.timestamp(), 257 | moment.num_seconds_from_midnight() as i64, 258 | ); 259 | 260 | assert_eq!(week_stats.peak_online, 40); 261 | assert_eq!(week_stats.peak_max, 50); 262 | 263 | assert_eq!( 264 | week_stats.daily_stats, 265 | [ 266 | RangeStats::default(), 267 | RangeStats { 268 | average_online: 26, 269 | peak_online: 40, 270 | peak_max: 40, 271 | }, 272 | RangeStats::default(), 273 | RangeStats::default(), 274 | RangeStats::default(), 275 | RangeStats::default(), 276 | RangeStats { 277 | average_online: 9, 278 | peak_online: 20, 279 | peak_max: 50, 280 | }, 281 | RangeStats { 282 | average_online: 10, 283 | peak_online: 15, 284 | peak_max: 30, 285 | }, 286 | ] 287 | ); 288 | 289 | let week_stats = data.week_stats(moment.timestamp(), 300); 290 | 291 | assert_eq!(week_stats.peak_online, 40); 292 | assert_eq!(week_stats.peak_max, 50); 293 | 294 | assert_eq!( 295 | week_stats.daily_stats, 296 | [ 297 | RangeStats { 298 | average_online: 13, 299 | peak_online: 13, 300 | peak_max: 40 301 | }, 302 | RangeStats { 303 | average_online: 40, 304 | peak_online: 40, 305 | peak_max: 40 306 | }, 307 | RangeStats::default(), 308 | RangeStats::default(), 309 | RangeStats::default(), 310 | RangeStats { 311 | average_online: 3, 312 | peak_online: 4, 313 | peak_max: 50 314 | }, 315 | RangeStats { 316 | average_online: 13, 317 | peak_online: 20, 318 | peak_max: 30 319 | }, 320 | RangeStats { 321 | average_online: 10, 322 | peak_online: 10, 323 | peak_max: 30 324 | } 325 | ] 326 | ); 327 | } 328 | 329 | // Test some aspects of interaction with the storage file 330 | #[test] 331 | fn file_handling() -> Result<(), anyhow::Error> { 332 | let tmp_dir = TempDir::new()?; 333 | let filepath = tmp_dir.path().join("week_stats"); 334 | 335 | // File doesn't exist 336 | assert!(!filepath.exists()); 337 | 338 | let _ = determine_week_stats(&filepath, 10, 40)?; 339 | 340 | // File exists now 341 | assert!(filepath.exists()); 342 | 343 | let stats = determine_week_stats(&filepath, 20, 50)?; 344 | assert_eq!(stats.peak_online, 20); 345 | 346 | // Corrupt the file 347 | fs::write(&filepath, "getrekt")?; 348 | 349 | // Make sure we recover and start the file over 350 | let stats = determine_week_stats(&filepath, 10, 40)?; 351 | assert_eq!(stats.peak_online, 10); 352 | 353 | Ok(()) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::CStr, 3 | fs, mem, 4 | os::raw::{c_uint, c_ulonglong}, 5 | panic, 6 | path::Path, 7 | time::Duration, 8 | }; 9 | use std::{ 10 | ffi::CString, 11 | os::raw::{c_char, c_longlong}, 12 | }; 13 | 14 | use anyhow::{anyhow, Context}; 15 | use identicon::{make_base64_identicon, IdenticonInput}; 16 | use mcping_common::{Player, Players, ProtocolType, Response, Version}; 17 | use serde::{Deserialize, Serialize}; 18 | use week_stats::{determine_week_stats, WeekStats}; 19 | 20 | pub mod identicon; 21 | pub mod mcping_common; 22 | #[cfg(test)] 23 | mod tests; 24 | mod week_stats; 25 | 26 | /// The overall status response. 27 | #[repr(C)] 28 | #[derive(Debug)] 29 | pub enum ServerStatus { 30 | /// The server was online and we got a valid ping response. 31 | Online(OnlineResponse), 32 | /// The server was offline and couldn't be reached, but we've been able to 33 | /// get a valid response from it before. 34 | /// 35 | /// This struct contains cached data. 36 | Offline(OfflineResponse), 37 | /// The server was offline and couldn't be reached, and we've never gotten 38 | /// a response from it previously. 39 | /// 40 | /// This struct contains an error message string and also ends up being a 41 | /// bit of a catch-all for any errors that can occur. In the future it could 42 | /// be nice to break this up into finer-grained variants (one for a legit 43 | /// error and one for the common case of an invalid server address). 44 | Unreachable(UnreachableResponse), 45 | } 46 | 47 | impl std::fmt::Display for ServerStatus { 48 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 49 | match self { 50 | ServerStatus::Online(r) => f.write_fmt(format_args!("Online: {}", r)), 51 | ServerStatus::Offline(r) => f.write_fmt(format_args!("Offline: {}", r)), 52 | ServerStatus::Unreachable(_) => f.write_str("Unreachable"), 53 | } 54 | } 55 | } 56 | 57 | #[repr(C)] 58 | #[derive(Debug)] 59 | pub struct OnlineResponse { 60 | /// The data obtained from the server's ping response. 61 | pub mcinfo: McInfoRaw, 62 | /// Statistics about the server over the past week or so. 63 | pub week_stats: WeekStats, 64 | } 65 | 66 | impl std::fmt::Display for OnlineResponse { 67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 68 | f.write_fmt(format_args!("{}", self.mcinfo)) 69 | } 70 | } 71 | 72 | #[repr(C)] 73 | #[derive(Debug)] 74 | pub struct OfflineResponse { 75 | /// The server's favicon (a cached copy or generated favicon). 76 | pub favicon: FaviconRaw, 77 | /// Statistics about the server over the past week or so. 78 | pub week_stats: WeekStats, 79 | } 80 | 81 | impl std::fmt::Display for OfflineResponse { 82 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 83 | f.write_fmt(format_args!("{}", self.favicon)) 84 | } 85 | } 86 | 87 | #[repr(C)] 88 | #[derive(Debug)] 89 | pub struct UnreachableResponse { 90 | /// An error string describing why the server wasn't reachable. 91 | pub error_string: *mut c_char, 92 | } 93 | 94 | /// Represents the format in which a favicon is cached on-disk. 95 | #[derive(Debug, Default, Serialize, Deserialize)] 96 | struct CachedFavicon { 97 | favicon: Option, 98 | } 99 | 100 | /// The server status response 101 | #[repr(C)] 102 | #[derive(Debug)] 103 | pub struct McInfoRaw { 104 | /// The protocol type of the successful ping. 105 | pub protocol_type: ProtocolType, 106 | /// Latency to the server 107 | pub latency: c_ulonglong, 108 | pub version: VersionRaw, 109 | /// Information about online players 110 | pub players: PlayersRaw, 111 | /// The server's description text 112 | pub description: *mut c_char, 113 | /// The server's favicon. 114 | pub favicon: FaviconRaw, 115 | } 116 | 117 | impl std::fmt::Display for McInfoRaw { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | f.debug_struct("McInfoRaw") 120 | .field("protocol_type", &self.protocol_type) 121 | .field("favicon", &format!("{}", self.favicon)) 122 | .finish() 123 | } 124 | } 125 | 126 | impl McInfoRaw { 127 | /// Build this struct from a server's ping response data and some data to build 128 | /// and identicon from if necessary. 129 | fn new(status: Response, identicon_input: IdenticonInput, always_use_identicon: bool) -> Self { 130 | let description = CString::new(status.motd).unwrap(); 131 | let favicon = FaviconRaw::from_data_and_options( 132 | status.favicon.as_deref(), 133 | identicon_input, 134 | always_use_identicon, 135 | ); 136 | 137 | Self { 138 | protocol_type: status.protocol_type, 139 | latency: status.latency, 140 | version: VersionRaw::from(status.version), 141 | players: PlayersRaw::from(status.players), 142 | description: description.into_raw(), 143 | favicon, 144 | } 145 | } 146 | } 147 | /// Trim off the non-base64 part of the favicon string to make it easier to get 148 | /// an image in Swift land. 149 | fn process_favicon(favicon: &str) -> &str { 150 | favicon.trim_start_matches("data:image/png;base64,") 151 | } 152 | 153 | /// Information about the server's version 154 | #[repr(C)] 155 | #[derive(Debug)] 156 | pub struct VersionRaw { 157 | /// The name of the version the server is running 158 | /// 159 | /// In practice this comes in a large variety of different formats. 160 | pub name: *mut c_char, 161 | /// See https://wiki.vg/Protocol_version_numbers 162 | pub protocol: c_longlong, 163 | } 164 | 165 | impl From for VersionRaw { 166 | fn from(version: Version) -> Self { 167 | let name = CString::new(version.name).unwrap(); 168 | Self { 169 | name: name.into_raw(), 170 | protocol: version.protocol.unwrap_or_default(), 171 | } 172 | } 173 | } 174 | 175 | #[repr(C)] 176 | #[derive(Debug)] 177 | pub struct PlayerRaw { 178 | /// The player's name 179 | pub name: *mut c_char, 180 | /// The player's UUID 181 | pub id: *mut c_char, 182 | } 183 | 184 | impl From for PlayerRaw { 185 | fn from(player: Player) -> Self { 186 | let name = CString::new(player.name).unwrap(); 187 | let id = CString::new(player.id).unwrap(); 188 | Self { 189 | name: name.into_raw(), 190 | id: id.into_raw(), 191 | } 192 | } 193 | } 194 | 195 | #[repr(C)] 196 | #[derive(Debug)] 197 | pub struct PlayersRaw { 198 | pub max: c_longlong, 199 | pub online: c_longlong, 200 | /// A preview of which players are online 201 | /// 202 | /// In practice servers often don't send this or use it for more advertising. 203 | /// This will be a null pointer if not present. 204 | pub sample: *mut PlayerRaw, 205 | pub sample_len: c_uint, 206 | } 207 | 208 | impl From for PlayersRaw { 209 | fn from(players: Players) -> Self { 210 | let (sample, sample_len) = if !players.sample.is_empty() { 211 | // Map into a vector of our repr(C) `Player` struct 212 | let mut sample = players 213 | .sample 214 | .into_iter() 215 | .map(PlayerRaw::from) 216 | .collect::>(); 217 | sample.shrink_to_fit(); 218 | assert!(sample.len() == sample.capacity()); 219 | let ptr = sample.as_mut_ptr(); 220 | let len = sample.len(); 221 | 222 | mem::forget(sample); 223 | 224 | (ptr, len) 225 | } else { 226 | (std::ptr::null_mut(), 0) 227 | }; 228 | 229 | Self { 230 | max: players.max, 231 | online: players.online, 232 | sample, 233 | sample_len: sample_len as _, 234 | } 235 | } 236 | } 237 | 238 | /// The server's favicon image. 239 | #[repr(C)] 240 | #[derive(Debug)] 241 | pub enum FaviconRaw { 242 | /// The server provided a favicon. 243 | ServerProvided(*mut c_char), 244 | /// We generated a favicon because the server didn't provide one. 245 | Generated(*mut c_char), 246 | /// There is no favicon image. 247 | NoFavicon, 248 | } 249 | 250 | impl std::fmt::Display for FaviconRaw { 251 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 252 | match self { 253 | FaviconRaw::ServerProvided(_) => f.write_str("ServerProvided"), 254 | FaviconRaw::Generated(_) => f.write_str("Generated"), 255 | FaviconRaw::NoFavicon => f.write_str("NoFavicon"), 256 | } 257 | } 258 | } 259 | 260 | impl FaviconRaw { 261 | /// Picks the best favicon based on the given data and options. 262 | fn from_data_and_options( 263 | server_favicon: Option<&str>, 264 | identicon_input: IdenticonInput, 265 | always_use_identicon: bool, 266 | ) -> Self { 267 | let make_generated = || { 268 | make_base64_identicon(identicon_input) 269 | .and_then(|s| CString::new(s).ok()) 270 | .map(|s| Self::Generated(s.into_raw())) 271 | .unwrap_or(Self::NoFavicon) 272 | }; 273 | 274 | if always_use_identicon { 275 | // Always generate an identicon 276 | make_generated() 277 | } else { 278 | // Try to use the server favicon and fallback to a generated identicon 279 | server_favicon 280 | .map(process_favicon) 281 | .and_then(|s| CString::new(s).ok()) 282 | .map(|s| Self::ServerProvided(s.into_raw())) 283 | .unwrap_or_else(make_generated) 284 | } 285 | } 286 | } 287 | 288 | /// Wrapper around `mcping_common::get_status`. 289 | /// 290 | /// This wrapper enables both offline and online testing. 291 | fn mcping_get_status_wrapper( 292 | address: String, 293 | timeout: Option, 294 | protocol_type: ProtocolType, 295 | ) -> Result { 296 | // Mock some responses for use during testing 297 | #[cfg(test)] 298 | { 299 | let mut response = Response { 300 | protocol_type: mcping_common::ProtocolType::Java, 301 | latency: 63, 302 | version: Version { 303 | name: "".to_string(), 304 | protocol: Some(187), 305 | }, 306 | players: Players { 307 | max: 200, 308 | online: 103, 309 | sample: vec![], 310 | }, 311 | motd: "".to_string(), 312 | favicon: None, 313 | }; 314 | 315 | match address.as_str() { 316 | "test.server.basic" => return Ok(response), 317 | "test.server.full" => { 318 | response.version.name = "something".to_string(); 319 | response.motd = "hello! description test".to_string(); 320 | response.favicon = Some("abase64string".to_string()); 321 | response.players.sample = vec![ 322 | Player { 323 | id: "1".to_string(), 324 | name: "test1".to_string(), 325 | }, 326 | Player { 327 | id: "2".to_string(), 328 | name: "test2".to_string(), 329 | }, 330 | ]; 331 | 332 | return Ok(response); 333 | } 334 | "test.server.dnslookupfails" => return Err(mcping::Error::DnsLookupFailed), 335 | _ => { 336 | // panic if online testing isn't enabled 337 | if cfg!(not(feature = "online")) { 338 | panic!("can only use mocked addresses while testing offline") 339 | } 340 | } 341 | } 342 | } 343 | 344 | mcping_common::get_status(address, timeout, protocol_type) 345 | } 346 | 347 | /// The rusty version of what we need to get done. 348 | /// 349 | /// The main logic of pinging a server and caching / processing the relevant data 350 | /// should be implemented here. It's perfectly okay to panic and return errors as 351 | /// needed. 352 | fn get_server_status_rust( 353 | address: &str, 354 | protocol_type: ProtocolType, 355 | always_use_identicon: bool, 356 | app_group_container: &str, 357 | ) -> Result { 358 | if address.is_empty() { 359 | // The following logic is meaningless if the server address is a blank 360 | // string 361 | return Err(anyhow!("empty server address")); 362 | } 363 | 364 | if app_group_container.is_empty() { 365 | // The following logic is meaningless if the app group container path 366 | // is blank 367 | return Err(anyhow!("empty app group container path")); 368 | } 369 | 370 | // Data for a specific server is stored within a folder specifically for 371 | // ping data, and within that a folder specifically for the address being 372 | // pinged. 373 | // 374 | // Note that the port will be a part of this address, so this will properly 375 | // handle multiple servers with the same IP / hostname but differing ports. 376 | // The server address is lowercased for optimal cache hits. It will not 377 | // handle unifying `mc.server.net` and `mc.server.net:25565`, though. 378 | let server_folder = Path::new(app_group_container) 379 | .join("mc_server_data") 380 | .join(format!( 381 | "{}_{}", 382 | address.to_lowercase().replace('.', "_").replace(':', "_"), 383 | protocol_type 384 | )); 385 | // Make sure the folders have been created 386 | fs::create_dir_all(&server_folder).with_context(|| { 387 | format!( 388 | "creating server folder(s): {}", 389 | server_folder.to_string_lossy() 390 | ) 391 | })?; 392 | 393 | let cached_favicon_path = server_folder.join("cached_favicon"); 394 | let week_stats_path = server_folder.join("week_stats"); 395 | // Drop `server_folder` so we don't accidentally use it again 396 | drop(server_folder); 397 | 398 | // Prepare the data to create identicons with if necessary 399 | let identicon_input = IdenticonInput { 400 | protocol_type, 401 | address, 402 | }; 403 | 404 | // A five-second timeout is used to avoid exceeding the amount of time our 405 | // widget process is given to run in. 406 | // 407 | // For example, this will end an attempt to ping "google.com" in about five 408 | // seconds; otherwise, we'd wait until the OS timed out the request, before 409 | // which time our process would likely end up being killed. This would 410 | // result in the widget being left in the placeholder view rather than 411 | // being updated with an error message. 412 | match mcping_get_status_wrapper( 413 | address.to_string(), 414 | Some(Duration::from_secs(5)), 415 | protocol_type, 416 | ) { 417 | Ok(status) => { 418 | // Cache the favicon 419 | let cached_favicon = CachedFavicon { 420 | favicon: status 421 | .favicon 422 | .as_deref() 423 | .map(process_favicon) 424 | .map(|s| s.to_owned()), 425 | }; 426 | let cached_favicon = serde_json::to_string(&cached_favicon)?; 427 | fs::write(&cached_favicon_path, &cached_favicon).with_context(|| { 428 | format!( 429 | "writing cached favicon struct to {}", 430 | cached_favicon_path.to_string_lossy() 431 | ) 432 | })?; 433 | 434 | // Handle week stats 435 | let week_stats = 436 | determine_week_stats(&week_stats_path, status.players.online, status.players.max)?; 437 | 438 | let mcinfo = McInfoRaw::new(status, identicon_input, always_use_identicon); 439 | Ok(ServerStatus::Online(OnlineResponse { mcinfo, week_stats })) 440 | } 441 | Err(e) => { 442 | if cached_favicon_path.exists() { 443 | let data = fs::read(&cached_favicon_path).with_context(|| { 444 | format!( 445 | "reading cached favicon data from {}", 446 | cached_favicon_path.to_string_lossy() 447 | ) 448 | })?; 449 | let cached_favicon: CachedFavicon = 450 | serde_json::from_slice(&data).with_context(|| { 451 | format!( 452 | "deserializing cached favicon data: {}", 453 | String::from_utf8(data).unwrap_or_else(|_| "invalid utf-8".to_string()) 454 | ) 455 | })?; 456 | 457 | let favicon = FaviconRaw::from_data_and_options( 458 | cached_favicon.favicon.as_deref(), 459 | identicon_input, 460 | always_use_identicon, 461 | ); 462 | 463 | // Handle week stats (server is offline, so just use zeroes) 464 | let week_stats = determine_week_stats(&week_stats_path, 0, 0)?; 465 | 466 | Ok(ServerStatus::Offline(OfflineResponse { 467 | favicon, 468 | week_stats, 469 | })) 470 | } else { 471 | Err(e.into()) 472 | } 473 | } 474 | } 475 | } 476 | 477 | /// This function is responsible for catching any panics that could possibly 478 | /// occur. 479 | fn get_server_status_catch_panic( 480 | address: *const c_char, 481 | protocol_type: ProtocolType, 482 | always_use_identicon: bool, 483 | app_group_container: *const c_char, 484 | ) -> Result { 485 | match panic::catch_unwind(|| { 486 | if address.is_null() { 487 | return Err(anyhow!("server address pointer was null")); 488 | } 489 | 490 | let address = unsafe { CStr::from_ptr(address) }; 491 | let address = address 492 | .to_str() 493 | .with_context(|| "converting server address from cstr to rust str")?; 494 | 495 | if app_group_container.is_null() { 496 | return Err(anyhow!("app group container pointer was null")); 497 | } 498 | 499 | let app_group_container = unsafe { CStr::from_ptr(app_group_container) }; 500 | let app_group_container = app_group_container 501 | .to_str() 502 | .with_context(|| "converting app group container from cstr to rust str")?; 503 | 504 | get_server_status_rust( 505 | address, 506 | protocol_type, 507 | always_use_identicon, 508 | app_group_container, 509 | ) 510 | }) { 511 | Ok(result) => Ok(result?), 512 | Err(e) => Err(anyhow!("a panic occurred in rust code: {:?}", e)), 513 | } 514 | } 515 | 516 | /// Ping a Minecraft server at the given `address`, working with data stored in 517 | /// the given `app_group_container`. 518 | /// 519 | /// # Safety 520 | /// 521 | /// The provided pointers must point to valid cstrings. 522 | #[no_mangle] 523 | pub unsafe extern "C" fn get_server_status( 524 | address: *const c_char, 525 | protocol_type: ProtocolType, 526 | always_use_identicon: bool, 527 | app_group_container: *const c_char, 528 | ) -> ServerStatus { 529 | match get_server_status_catch_panic( 530 | address, 531 | protocol_type, 532 | always_use_identicon, 533 | app_group_container, 534 | ) { 535 | Ok(status) => status, 536 | Err(e) => { 537 | // Note that we need to be careful not to panic here 538 | let error_string = format!("failed to ping server: {}", e); 539 | let error_string = CString::new(error_string).unwrap_or_default(); 540 | 541 | ServerStatus::Unreachable(UnreachableResponse { 542 | error_string: error_string.into_raw(), 543 | }) 544 | } 545 | } 546 | } 547 | 548 | #[no_mangle] 549 | pub extern "C" fn free_status_response(response: ServerStatus) { 550 | match response { 551 | ServerStatus::Online(OnlineResponse { mcinfo, week_stats }) => { 552 | free_mcinfo(mcinfo); 553 | // `WeekStats` doesn't have any heap-allocated stuff, so we don't need 554 | // to free it 555 | drop(week_stats); 556 | } 557 | ServerStatus::Offline(OfflineResponse { 558 | favicon, 559 | week_stats, 560 | }) => { 561 | free_favicon(favicon); 562 | // `WeekStats` doesn't have any heap-allocated stuff, so we don't need 563 | // to free it 564 | drop(week_stats); 565 | } 566 | ServerStatus::Unreachable(UnreachableResponse { error_string }) => { 567 | if !error_string.is_null() { 568 | let _ = unsafe { CString::from_raw(error_string) }; 569 | } 570 | } 571 | } 572 | } 573 | 574 | #[no_mangle] 575 | pub extern "C" fn free_mcinfo(mcinfo: McInfoRaw) { 576 | let _ = unsafe { CString::from_raw(mcinfo.description) }; 577 | 578 | free_favicon(mcinfo.favicon); 579 | 580 | let _ = unsafe { CString::from_raw(mcinfo.version.name) }; 581 | 582 | if !mcinfo.players.sample.is_null() { 583 | let sample = unsafe { 584 | Vec::from_raw_parts( 585 | mcinfo.players.sample, 586 | mcinfo.players.sample_len as _, 587 | mcinfo.players.sample_len as _, 588 | ) 589 | }; 590 | 591 | for player in sample.iter() { 592 | let _ = unsafe { CString::from_raw(player.name) }; 593 | let _ = unsafe { CString::from_raw(player.id) }; 594 | } 595 | } 596 | } 597 | 598 | #[no_mangle] 599 | pub extern "C" fn free_favicon(favicon: FaviconRaw) { 600 | match favicon { 601 | FaviconRaw::ServerProvided(p) | FaviconRaw::Generated(p) => { 602 | if !p.is_null() { 603 | let _ = unsafe { CString::from_raw(p) }; 604 | } 605 | } 606 | FaviconRaw::NoFavicon => {} 607 | } 608 | } 609 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "adler" 5 | version = "1.0.2" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 8 | 9 | [[package]] 10 | name = "adler32" 11 | version = "1.2.0" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 14 | 15 | [[package]] 16 | name = "anyhow" 17 | version = "1.0.40" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" 20 | 21 | [[package]] 22 | name = "approx" 23 | version = "0.3.2" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" 26 | dependencies = [ 27 | "num-traits", 28 | ] 29 | 30 | [[package]] 31 | name = "async-trait" 32 | version = "0.1.48" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf" 35 | dependencies = [ 36 | "proc-macro2", 37 | "quote", 38 | "syn", 39 | ] 40 | 41 | [[package]] 42 | name = "autocfg" 43 | version = "1.0.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 46 | 47 | [[package]] 48 | name = "base64" 49 | version = "0.13.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "1.2.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 58 | 59 | [[package]] 60 | name = "block-buffer" 61 | version = "0.9.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 64 | dependencies = [ 65 | "generic-array", 66 | ] 67 | 68 | [[package]] 69 | name = "bytemuck" 70 | version = "1.5.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" 73 | 74 | [[package]] 75 | name = "byteorder" 76 | version = "1.4.3" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 79 | 80 | [[package]] 81 | name = "cfg-if" 82 | version = "1.0.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 85 | 86 | [[package]] 87 | name = "chrono" 88 | version = "0.4.19" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 91 | dependencies = [ 92 | "libc", 93 | "num-integer", 94 | "num-traits", 95 | "serde", 96 | "time", 97 | "winapi", 98 | ] 99 | 100 | [[package]] 101 | name = "color_quant" 102 | version = "1.1.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 105 | 106 | [[package]] 107 | name = "cpuid-bool" 108 | version = "0.1.2" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" 111 | 112 | [[package]] 113 | name = "crc32fast" 114 | version = "1.2.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" 117 | dependencies = [ 118 | "cfg-if", 119 | ] 120 | 121 | [[package]] 122 | name = "crossbeam-channel" 123 | version = "0.5.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" 126 | dependencies = [ 127 | "cfg-if", 128 | "crossbeam-utils", 129 | ] 130 | 131 | [[package]] 132 | name = "crossbeam-deque" 133 | version = "0.8.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" 136 | dependencies = [ 137 | "cfg-if", 138 | "crossbeam-epoch", 139 | "crossbeam-utils", 140 | ] 141 | 142 | [[package]] 143 | name = "crossbeam-epoch" 144 | version = "0.9.3" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" 147 | dependencies = [ 148 | "cfg-if", 149 | "crossbeam-utils", 150 | "lazy_static", 151 | "memoffset", 152 | "scopeguard", 153 | ] 154 | 155 | [[package]] 156 | name = "crossbeam-utils" 157 | version = "0.8.3" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" 160 | dependencies = [ 161 | "autocfg", 162 | "cfg-if", 163 | "lazy_static", 164 | ] 165 | 166 | [[package]] 167 | name = "data-encoding" 168 | version = "2.3.2" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" 171 | 172 | [[package]] 173 | name = "deflate" 174 | version = "0.8.6" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" 177 | dependencies = [ 178 | "adler32", 179 | "byteorder", 180 | ] 181 | 182 | [[package]] 183 | name = "digest" 184 | version = "0.9.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 187 | dependencies = [ 188 | "generic-array", 189 | ] 190 | 191 | [[package]] 192 | name = "dissimilar" 193 | version = "1.0.2" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "fc4b29f4b9bb94bf267d57269fd0706d343a160937108e9619fe380645428abb" 196 | 197 | [[package]] 198 | name = "either" 199 | version = "1.6.1" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 202 | 203 | [[package]] 204 | name = "enum-as-inner" 205 | version = "0.3.3" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" 208 | dependencies = [ 209 | "heck", 210 | "proc-macro2", 211 | "quote", 212 | "syn", 213 | ] 214 | 215 | [[package]] 216 | name = "expect-test" 217 | version = "1.1.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "2300477aab3a378f2ca00a4fbd4dc713654ab7ed790e4017493cb33656280633" 220 | dependencies = [ 221 | "dissimilar", 222 | "once_cell", 223 | ] 224 | 225 | [[package]] 226 | name = "form_urlencoded" 227 | version = "1.0.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 230 | dependencies = [ 231 | "matches", 232 | "percent-encoding", 233 | ] 234 | 235 | [[package]] 236 | name = "futures-channel" 237 | version = "0.3.14" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25" 240 | dependencies = [ 241 | "futures-core", 242 | ] 243 | 244 | [[package]] 245 | name = "futures-core" 246 | version = "0.3.14" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815" 249 | 250 | [[package]] 251 | name = "futures-io" 252 | version = "0.3.14" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04" 255 | 256 | [[package]] 257 | name = "futures-task" 258 | version = "0.3.14" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc" 261 | 262 | [[package]] 263 | name = "futures-util" 264 | version = "0.3.14" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025" 267 | dependencies = [ 268 | "futures-core", 269 | "futures-task", 270 | "pin-project-lite", 271 | "pin-utils", 272 | "slab", 273 | ] 274 | 275 | [[package]] 276 | name = "generic-array" 277 | version = "0.14.4" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" 280 | dependencies = [ 281 | "typenum", 282 | "version_check", 283 | ] 284 | 285 | [[package]] 286 | name = "getrandom" 287 | version = "0.1.16" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 290 | dependencies = [ 291 | "cfg-if", 292 | "libc", 293 | "wasi 0.9.0+wasi-snapshot-preview1", 294 | ] 295 | 296 | [[package]] 297 | name = "getrandom" 298 | version = "0.2.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" 301 | dependencies = [ 302 | "cfg-if", 303 | "libc", 304 | "wasi 0.10.2+wasi-snapshot-preview1", 305 | ] 306 | 307 | [[package]] 308 | name = "gif" 309 | version = "0.11.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" 312 | dependencies = [ 313 | "color_quant", 314 | "weezl", 315 | ] 316 | 317 | [[package]] 318 | name = "heck" 319 | version = "0.3.2" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" 322 | dependencies = [ 323 | "unicode-segmentation", 324 | ] 325 | 326 | [[package]] 327 | name = "hermit-abi" 328 | version = "0.1.18" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 331 | dependencies = [ 332 | "libc", 333 | ] 334 | 335 | [[package]] 336 | name = "hostname" 337 | version = "0.3.1" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" 340 | dependencies = [ 341 | "libc", 342 | "match_cfg", 343 | "winapi", 344 | ] 345 | 346 | [[package]] 347 | name = "identicon-rs" 348 | version = "2.0.2" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "cee84d4fca7be7c46a45ed4d26068bd4eeb18a853242b6080ee46cdb63ce4367" 351 | dependencies = [ 352 | "image", 353 | "palette", 354 | "sha2", 355 | ] 356 | 357 | [[package]] 358 | name = "idna" 359 | version = "0.2.2" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" 362 | dependencies = [ 363 | "matches", 364 | "unicode-bidi", 365 | "unicode-normalization", 366 | ] 367 | 368 | [[package]] 369 | name = "image" 370 | version = "0.23.14" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" 373 | dependencies = [ 374 | "bytemuck", 375 | "byteorder", 376 | "color_quant", 377 | "gif", 378 | "jpeg-decoder", 379 | "num-iter", 380 | "num-rational", 381 | "num-traits", 382 | "png", 383 | "scoped_threadpool", 384 | "tiff", 385 | ] 386 | 387 | [[package]] 388 | name = "instant" 389 | version = "0.1.9" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" 392 | dependencies = [ 393 | "cfg-if", 394 | ] 395 | 396 | [[package]] 397 | name = "ipconfig" 398 | version = "0.2.2" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" 401 | dependencies = [ 402 | "socket2", 403 | "widestring", 404 | "winapi", 405 | "winreg", 406 | ] 407 | 408 | [[package]] 409 | name = "ipnet" 410 | version = "2.3.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" 413 | 414 | [[package]] 415 | name = "itoa" 416 | version = "0.4.7" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 419 | 420 | [[package]] 421 | name = "jpeg-decoder" 422 | version = "0.1.22" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" 425 | dependencies = [ 426 | "rayon", 427 | ] 428 | 429 | [[package]] 430 | name = "lazy_static" 431 | version = "1.4.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 434 | 435 | [[package]] 436 | name = "libc" 437 | version = "0.2.93" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" 440 | 441 | [[package]] 442 | name = "linked-hash-map" 443 | version = "0.5.4" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" 446 | 447 | [[package]] 448 | name = "lock_api" 449 | version = "0.4.3" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" 452 | dependencies = [ 453 | "scopeguard", 454 | ] 455 | 456 | [[package]] 457 | name = "log" 458 | version = "0.4.14" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 461 | dependencies = [ 462 | "cfg-if", 463 | ] 464 | 465 | [[package]] 466 | name = "lru-cache" 467 | version = "0.1.2" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" 470 | dependencies = [ 471 | "linked-hash-map", 472 | ] 473 | 474 | [[package]] 475 | name = "match_cfg" 476 | version = "0.1.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" 479 | 480 | [[package]] 481 | name = "matches" 482 | version = "0.1.8" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 485 | 486 | [[package]] 487 | name = "mcping" 488 | version = "0.2.0" 489 | source = "git+https://github.com/Scetch/mcping.git#7e74d2220a61557b5514768edb7da42d2758e143" 490 | dependencies = [ 491 | "byteorder", 492 | "rand 0.8.3", 493 | "serde", 494 | "serde_json", 495 | "thiserror", 496 | "trust-dns-resolver", 497 | ] 498 | 499 | [[package]] 500 | name = "memoffset" 501 | version = "0.6.3" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" 504 | dependencies = [ 505 | "autocfg", 506 | ] 507 | 508 | [[package]] 509 | name = "minecraft-status" 510 | version = "0.1.0" 511 | dependencies = [ 512 | "anyhow", 513 | "base64", 514 | "chrono", 515 | "expect-test", 516 | "identicon-rs", 517 | "image", 518 | "mcping", 519 | "serde", 520 | "serde_json", 521 | "tempfile", 522 | ] 523 | 524 | [[package]] 525 | name = "miniz_oxide" 526 | version = "0.3.7" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" 529 | dependencies = [ 530 | "adler32", 531 | ] 532 | 533 | [[package]] 534 | name = "miniz_oxide" 535 | version = "0.4.4" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 538 | dependencies = [ 539 | "adler", 540 | "autocfg", 541 | ] 542 | 543 | [[package]] 544 | name = "mio" 545 | version = "0.7.11" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" 548 | dependencies = [ 549 | "libc", 550 | "log", 551 | "miow", 552 | "ntapi", 553 | "winapi", 554 | ] 555 | 556 | [[package]] 557 | name = "miow" 558 | version = "0.3.7" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 561 | dependencies = [ 562 | "winapi", 563 | ] 564 | 565 | [[package]] 566 | name = "ntapi" 567 | version = "0.3.6" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 570 | dependencies = [ 571 | "winapi", 572 | ] 573 | 574 | [[package]] 575 | name = "num-integer" 576 | version = "0.1.44" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 579 | dependencies = [ 580 | "autocfg", 581 | "num-traits", 582 | ] 583 | 584 | [[package]] 585 | name = "num-iter" 586 | version = "0.1.42" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 589 | dependencies = [ 590 | "autocfg", 591 | "num-integer", 592 | "num-traits", 593 | ] 594 | 595 | [[package]] 596 | name = "num-rational" 597 | version = "0.3.2" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" 600 | dependencies = [ 601 | "autocfg", 602 | "num-integer", 603 | "num-traits", 604 | ] 605 | 606 | [[package]] 607 | name = "num-traits" 608 | version = "0.2.14" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 611 | dependencies = [ 612 | "autocfg", 613 | ] 614 | 615 | [[package]] 616 | name = "num_cpus" 617 | version = "1.13.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 620 | dependencies = [ 621 | "hermit-abi", 622 | "libc", 623 | ] 624 | 625 | [[package]] 626 | name = "once_cell" 627 | version = "1.7.2" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 630 | 631 | [[package]] 632 | name = "opaque-debug" 633 | version = "0.3.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 636 | 637 | [[package]] 638 | name = "palette" 639 | version = "0.5.0" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "a05c0334468e62a4dfbda34b29110aa7d70d58c7fdb2c9857b5874dd9827cc59" 642 | dependencies = [ 643 | "approx", 644 | "num-traits", 645 | "palette_derive", 646 | "phf", 647 | "phf_codegen", 648 | ] 649 | 650 | [[package]] 651 | name = "palette_derive" 652 | version = "0.5.0" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "0b4b5f600e60dd3a147fb57b4547033d382d1979eb087af310e91cb45a63b1f4" 655 | dependencies = [ 656 | "proc-macro2", 657 | "quote", 658 | "syn", 659 | ] 660 | 661 | [[package]] 662 | name = "parking_lot" 663 | version = "0.11.1" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" 666 | dependencies = [ 667 | "instant", 668 | "lock_api", 669 | "parking_lot_core", 670 | ] 671 | 672 | [[package]] 673 | name = "parking_lot_core" 674 | version = "0.8.3" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" 677 | dependencies = [ 678 | "cfg-if", 679 | "instant", 680 | "libc", 681 | "redox_syscall", 682 | "smallvec", 683 | "winapi", 684 | ] 685 | 686 | [[package]] 687 | name = "percent-encoding" 688 | version = "2.1.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 691 | 692 | [[package]] 693 | name = "phf" 694 | version = "0.8.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" 697 | dependencies = [ 698 | "phf_shared", 699 | ] 700 | 701 | [[package]] 702 | name = "phf_codegen" 703 | version = "0.8.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" 706 | dependencies = [ 707 | "phf_generator", 708 | "phf_shared", 709 | ] 710 | 711 | [[package]] 712 | name = "phf_generator" 713 | version = "0.8.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" 716 | dependencies = [ 717 | "phf_shared", 718 | "rand 0.7.3", 719 | ] 720 | 721 | [[package]] 722 | name = "phf_shared" 723 | version = "0.8.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" 726 | dependencies = [ 727 | "siphasher", 728 | ] 729 | 730 | [[package]] 731 | name = "pin-project-lite" 732 | version = "0.2.6" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 735 | 736 | [[package]] 737 | name = "pin-utils" 738 | version = "0.1.0" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 741 | 742 | [[package]] 743 | name = "png" 744 | version = "0.16.8" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" 747 | dependencies = [ 748 | "bitflags", 749 | "crc32fast", 750 | "deflate", 751 | "miniz_oxide 0.3.7", 752 | ] 753 | 754 | [[package]] 755 | name = "ppv-lite86" 756 | version = "0.2.10" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 759 | 760 | [[package]] 761 | name = "proc-macro2" 762 | version = "1.0.26" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" 765 | dependencies = [ 766 | "unicode-xid", 767 | ] 768 | 769 | [[package]] 770 | name = "quick-error" 771 | version = "1.2.3" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 774 | 775 | [[package]] 776 | name = "quote" 777 | version = "1.0.9" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 780 | dependencies = [ 781 | "proc-macro2", 782 | ] 783 | 784 | [[package]] 785 | name = "rand" 786 | version = "0.7.3" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 789 | dependencies = [ 790 | "getrandom 0.1.16", 791 | "libc", 792 | "rand_chacha 0.2.2", 793 | "rand_core 0.5.1", 794 | "rand_hc 0.2.0", 795 | "rand_pcg", 796 | ] 797 | 798 | [[package]] 799 | name = "rand" 800 | version = "0.8.3" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" 803 | dependencies = [ 804 | "libc", 805 | "rand_chacha 0.3.0", 806 | "rand_core 0.6.2", 807 | "rand_hc 0.3.0", 808 | ] 809 | 810 | [[package]] 811 | name = "rand_chacha" 812 | version = "0.2.2" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 815 | dependencies = [ 816 | "ppv-lite86", 817 | "rand_core 0.5.1", 818 | ] 819 | 820 | [[package]] 821 | name = "rand_chacha" 822 | version = "0.3.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" 825 | dependencies = [ 826 | "ppv-lite86", 827 | "rand_core 0.6.2", 828 | ] 829 | 830 | [[package]] 831 | name = "rand_core" 832 | version = "0.5.1" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 835 | dependencies = [ 836 | "getrandom 0.1.16", 837 | ] 838 | 839 | [[package]] 840 | name = "rand_core" 841 | version = "0.6.2" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" 844 | dependencies = [ 845 | "getrandom 0.2.2", 846 | ] 847 | 848 | [[package]] 849 | name = "rand_hc" 850 | version = "0.2.0" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 853 | dependencies = [ 854 | "rand_core 0.5.1", 855 | ] 856 | 857 | [[package]] 858 | name = "rand_hc" 859 | version = "0.3.0" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" 862 | dependencies = [ 863 | "rand_core 0.6.2", 864 | ] 865 | 866 | [[package]] 867 | name = "rand_pcg" 868 | version = "0.2.1" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" 871 | dependencies = [ 872 | "rand_core 0.5.1", 873 | ] 874 | 875 | [[package]] 876 | name = "rayon" 877 | version = "1.5.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" 880 | dependencies = [ 881 | "autocfg", 882 | "crossbeam-deque", 883 | "either", 884 | "rayon-core", 885 | ] 886 | 887 | [[package]] 888 | name = "rayon-core" 889 | version = "1.9.0" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" 892 | dependencies = [ 893 | "crossbeam-channel", 894 | "crossbeam-deque", 895 | "crossbeam-utils", 896 | "lazy_static", 897 | "num_cpus", 898 | ] 899 | 900 | [[package]] 901 | name = "redox_syscall" 902 | version = "0.2.6" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" 905 | dependencies = [ 906 | "bitflags", 907 | ] 908 | 909 | [[package]] 910 | name = "remove_dir_all" 911 | version = "0.5.3" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 914 | dependencies = [ 915 | "winapi", 916 | ] 917 | 918 | [[package]] 919 | name = "resolv-conf" 920 | version = "0.7.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" 923 | dependencies = [ 924 | "hostname", 925 | "quick-error", 926 | ] 927 | 928 | [[package]] 929 | name = "ryu" 930 | version = "1.0.5" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 933 | 934 | [[package]] 935 | name = "scoped_threadpool" 936 | version = "0.1.9" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" 939 | 940 | [[package]] 941 | name = "scopeguard" 942 | version = "1.1.0" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 945 | 946 | [[package]] 947 | name = "serde" 948 | version = "1.0.125" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" 951 | dependencies = [ 952 | "serde_derive", 953 | ] 954 | 955 | [[package]] 956 | name = "serde_derive" 957 | version = "1.0.125" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" 960 | dependencies = [ 961 | "proc-macro2", 962 | "quote", 963 | "syn", 964 | ] 965 | 966 | [[package]] 967 | name = "serde_json" 968 | version = "1.0.64" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 971 | dependencies = [ 972 | "itoa", 973 | "ryu", 974 | "serde", 975 | ] 976 | 977 | [[package]] 978 | name = "sha2" 979 | version = "0.9.3" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" 982 | dependencies = [ 983 | "block-buffer", 984 | "cfg-if", 985 | "cpuid-bool", 986 | "digest", 987 | "opaque-debug", 988 | ] 989 | 990 | [[package]] 991 | name = "siphasher" 992 | version = "0.3.5" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" 995 | 996 | [[package]] 997 | name = "slab" 998 | version = "0.4.2" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 1001 | 1002 | [[package]] 1003 | name = "smallvec" 1004 | version = "1.6.1" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 1007 | 1008 | [[package]] 1009 | name = "socket2" 1010 | version = "0.3.19" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 1013 | dependencies = [ 1014 | "cfg-if", 1015 | "libc", 1016 | "winapi", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "syn" 1021 | version = "1.0.69" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" 1024 | dependencies = [ 1025 | "proc-macro2", 1026 | "quote", 1027 | "unicode-xid", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "tempfile" 1032 | version = "3.2.0" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 1035 | dependencies = [ 1036 | "cfg-if", 1037 | "libc", 1038 | "rand 0.8.3", 1039 | "redox_syscall", 1040 | "remove_dir_all", 1041 | "winapi", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "thiserror" 1046 | version = "1.0.24" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" 1049 | dependencies = [ 1050 | "thiserror-impl", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "thiserror-impl" 1055 | version = "1.0.24" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" 1058 | dependencies = [ 1059 | "proc-macro2", 1060 | "quote", 1061 | "syn", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "tiff" 1066 | version = "0.6.1" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" 1069 | dependencies = [ 1070 | "jpeg-decoder", 1071 | "miniz_oxide 0.4.4", 1072 | "weezl", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "time" 1077 | version = "0.1.43" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 1080 | dependencies = [ 1081 | "libc", 1082 | "winapi", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "tinyvec" 1087 | version = "1.2.0" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 1090 | dependencies = [ 1091 | "tinyvec_macros", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "tinyvec_macros" 1096 | version = "0.1.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1099 | 1100 | [[package]] 1101 | name = "tokio" 1102 | version = "1.5.0" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" 1105 | dependencies = [ 1106 | "autocfg", 1107 | "libc", 1108 | "mio", 1109 | "num_cpus", 1110 | "pin-project-lite", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "trust-dns-proto" 1115 | version = "0.20.1" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "8d57e219ba600dd96c2f6d82eb79645068e14edbc5c7e27514af40436b88150c" 1118 | dependencies = [ 1119 | "async-trait", 1120 | "cfg-if", 1121 | "data-encoding", 1122 | "enum-as-inner", 1123 | "futures-channel", 1124 | "futures-io", 1125 | "futures-util", 1126 | "idna", 1127 | "ipnet", 1128 | "lazy_static", 1129 | "log", 1130 | "rand 0.8.3", 1131 | "smallvec", 1132 | "thiserror", 1133 | "tinyvec", 1134 | "tokio", 1135 | "url", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "trust-dns-resolver" 1140 | version = "0.20.1" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "b0437eea3a6da51acc1e946545ff53d5b8fb2611ff1c3bed58522dde100536ae" 1143 | dependencies = [ 1144 | "cfg-if", 1145 | "futures-util", 1146 | "ipconfig", 1147 | "lazy_static", 1148 | "log", 1149 | "lru-cache", 1150 | "parking_lot", 1151 | "resolv-conf", 1152 | "smallvec", 1153 | "thiserror", 1154 | "tokio", 1155 | "trust-dns-proto", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "typenum" 1160 | version = "1.13.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" 1163 | 1164 | [[package]] 1165 | name = "unicode-bidi" 1166 | version = "0.3.5" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 1169 | dependencies = [ 1170 | "matches", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "unicode-normalization" 1175 | version = "0.1.17" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" 1178 | dependencies = [ 1179 | "tinyvec", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "unicode-segmentation" 1184 | version = "1.7.1" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 1187 | 1188 | [[package]] 1189 | name = "unicode-xid" 1190 | version = "0.2.1" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 1193 | 1194 | [[package]] 1195 | name = "url" 1196 | version = "2.2.1" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" 1199 | dependencies = [ 1200 | "form_urlencoded", 1201 | "idna", 1202 | "matches", 1203 | "percent-encoding", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "version_check" 1208 | version = "0.9.3" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1211 | 1212 | [[package]] 1213 | name = "wasi" 1214 | version = "0.9.0+wasi-snapshot-preview1" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1217 | 1218 | [[package]] 1219 | name = "wasi" 1220 | version = "0.10.2+wasi-snapshot-preview1" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1223 | 1224 | [[package]] 1225 | name = "weezl" 1226 | version = "0.1.4" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "4a32b378380f4e9869b22f0b5177c68a5519f03b3454fde0b291455ddbae266c" 1229 | 1230 | [[package]] 1231 | name = "widestring" 1232 | version = "0.4.3" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" 1235 | 1236 | [[package]] 1237 | name = "winapi" 1238 | version = "0.3.9" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1241 | dependencies = [ 1242 | "winapi-i686-pc-windows-gnu", 1243 | "winapi-x86_64-pc-windows-gnu", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "winapi-i686-pc-windows-gnu" 1248 | version = "0.4.0" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1251 | 1252 | [[package]] 1253 | name = "winapi-x86_64-pc-windows-gnu" 1254 | version = "0.4.0" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1257 | 1258 | [[package]] 1259 | name = "winreg" 1260 | version = "0.6.2" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" 1263 | dependencies = [ 1264 | "winapi", 1265 | ] 1266 | -------------------------------------------------------------------------------- /swift/MinecraftStatus.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4E1FCB35259BF07B0018AAA3 /* minecraft_status.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E1FCB34259BF07B0018AAA3 /* minecraft_status.a */; }; 11 | 4E1FCB36259BF07B0018AAA3 /* minecraft_status.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E1FCB34259BF07B0018AAA3 /* minecraft_status.a */; }; 12 | 4E258F24259AC7D90093B65F /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E258F23259AC7D90093B65F /* WidgetKit.framework */; }; 13 | 4E258F26259AC7D90093B65F /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E258F25259AC7D90093B65F /* SwiftUI.framework */; }; 14 | 4E258F29259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E258F28259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.swift */; }; 15 | 4E258F2C259AC7DA0093B65F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4E258F2B259AC7DA0093B65F /* Assets.xcassets */; }; 16 | 4E258F2E259AC7DA0093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 4E258F2A259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition */; }; 17 | 4E258F2F259AC7DA0093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 4E258F2A259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition */; }; 18 | 4E258F32259AC7DA0093B65F /* MinecraftStatusWidgetViewExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4E258F21259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 19 | 4E258F51259ACBA90093B65F /* McStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6EC426259AAA58004B142B /* McStructs.swift */; }; 20 | 4E258F5B259BCD300093B65F /* PreviewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E258F5A259BCD300093B65F /* PreviewData.swift */; }; 21 | 4E308A8825915FB80073C5D7 /* MinecraftStatusApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E308A8725915FB80073C5D7 /* MinecraftStatusApp.swift */; }; 22 | 4E308A8A25915FB80073C5D7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E308A8925915FB80073C5D7 /* SettingsView.swift */; }; 23 | 4E308A8C25915FBA0073C5D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4E308A8B25915FBA0073C5D7 /* Assets.xcassets */; }; 24 | 4E308A8F25915FBA0073C5D7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4E308A8E25915FBA0073C5D7 /* Preview Assets.xcassets */; }; 25 | 4E308A9A25915FBA0073C5D7 /* MinecraftStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E308A9925915FBA0073C5D7 /* MinecraftStatusTests.swift */; }; 26 | 4E308AA525915FBA0073C5D7 /* MinecraftStatusUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E308AA425915FBA0073C5D7 /* MinecraftStatusUITests.swift */; }; 27 | 4E3C08DE259C1DBB00303A35 /* minecraft.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4E3C08DD259C1DBB00303A35 /* minecraft.ttf */; }; 28 | 4E3C08DF259C1DBB00303A35 /* minecraft.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4E3C08DD259C1DBB00303A35 /* minecraft.ttf */; }; 29 | 4E6EC427259AAA58004B142B /* McStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6EC426259AAA58004B142B /* McStructs.swift */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | 4E258F30259AC7DA0093B65F /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 4E308A7C25915FB80073C5D7 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 4E258F20259AC7D90093B65F; 38 | remoteInfo = MinecraftStatusWidgetViewExtension; 39 | }; 40 | 4E308A9625915FBA0073C5D7 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 4E308A7C25915FB80073C5D7 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 4E308A8325915FB80073C5D7; 45 | remoteInfo = MinecraftStatus; 46 | }; 47 | 4E308AA125915FBA0073C5D7 /* PBXContainerItemProxy */ = { 48 | isa = PBXContainerItemProxy; 49 | containerPortal = 4E308A7C25915FB80073C5D7 /* Project object */; 50 | proxyType = 1; 51 | remoteGlobalIDString = 4E308A8325915FB80073C5D7; 52 | remoteInfo = MinecraftStatus; 53 | }; 54 | /* End PBXContainerItemProxy section */ 55 | 56 | /* Begin PBXCopyFilesBuildPhase section */ 57 | 4E258F33259AC7DA0093B65F /* Embed App Extensions */ = { 58 | isa = PBXCopyFilesBuildPhase; 59 | buildActionMask = 2147483647; 60 | dstPath = ""; 61 | dstSubfolderSpec = 13; 62 | files = ( 63 | 4E258F32259AC7DA0093B65F /* MinecraftStatusWidgetViewExtension.appex in Embed App Extensions */, 64 | ); 65 | name = "Embed App Extensions"; 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXCopyFilesBuildPhase section */ 69 | 70 | /* Begin PBXFileReference section */ 71 | 4E1FCB34259BF07B0018AAA3 /* minecraft_status.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = minecraft_status.a; sourceTree = ""; }; 72 | 4E1FCB3F259BF1350018AAA3 /* minecraft_status.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = minecraft_status.h; sourceTree = ""; }; 73 | 4E258F21259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MinecraftStatusWidgetViewExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 74 | 4E258F23259AC7D90093B65F /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 75 | 4E258F25259AC7D90093B65F /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 76 | 4E258F28259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinecraftStatusWidgetViewExtension.swift; sourceTree = ""; }; 77 | 4E258F2A259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = MinecraftStatusWidgetViewExtension.intentdefinition; sourceTree = ""; }; 78 | 4E258F2B259AC7DA0093B65F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 79 | 4E258F2D259AC7DA0093B65F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 80 | 4E258F5A259BCD300093B65F /* PreviewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewData.swift; sourceTree = ""; }; 81 | 4E308A8425915FB80073C5D7 /* MinecraftStatus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MinecraftStatus.app; sourceTree = BUILT_PRODUCTS_DIR; }; 82 | 4E308A8725915FB80073C5D7 /* MinecraftStatusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinecraftStatusApp.swift; sourceTree = ""; }; 83 | 4E308A8925915FB80073C5D7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 84 | 4E308A8B25915FBA0073C5D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 85 | 4E308A8E25915FBA0073C5D7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 86 | 4E308A9025915FBA0073C5D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 87 | 4E308A9525915FBA0073C5D7 /* MinecraftStatusTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MinecraftStatusTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 88 | 4E308A9925915FBA0073C5D7 /* MinecraftStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinecraftStatusTests.swift; sourceTree = ""; }; 89 | 4E308A9B25915FBA0073C5D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 90 | 4E308AA025915FBA0073C5D7 /* MinecraftStatusUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MinecraftStatusUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 91 | 4E308AA425915FBA0073C5D7 /* MinecraftStatusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinecraftStatusUITests.swift; sourceTree = ""; }; 92 | 4E308AA625915FBA0073C5D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 93 | 4E3C08DD259C1DBB00303A35 /* minecraft.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = minecraft.ttf; sourceTree = ""; }; 94 | 4E3C08E9259C23C300303A35 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 95 | 4E3C153B259160C800549980 /* Minecraft-Status-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Minecraft-Status-Bridging-Header.h"; sourceTree = ""; }; 96 | 4E4C387625D8E49C00D8C18B /* MinecraftStatus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MinecraftStatus.entitlements; sourceTree = ""; }; 97 | 4E4C387B25D8E4EB00D8C18B /* MinecraftStatusWidgetViewExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MinecraftStatusWidgetViewExtension.entitlements; sourceTree = ""; }; 98 | 4E6EC426259AAA58004B142B /* McStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = McStructs.swift; sourceTree = ""; }; 99 | /* End PBXFileReference section */ 100 | 101 | /* Begin PBXFrameworksBuildPhase section */ 102 | 4E258F1E259AC7D90093B65F /* Frameworks */ = { 103 | isa = PBXFrameworksBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | 4E258F26259AC7D90093B65F /* SwiftUI.framework in Frameworks */, 107 | 4E258F24259AC7D90093B65F /* WidgetKit.framework in Frameworks */, 108 | 4E1FCB36259BF07B0018AAA3 /* minecraft_status.a in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | 4E308A8125915FB80073C5D7 /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | 4E1FCB35259BF07B0018AAA3 /* minecraft_status.a in Frameworks */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | 4E308A9225915FBA0073C5D7 /* Frameworks */ = { 121 | isa = PBXFrameworksBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | ); 125 | runOnlyForDeploymentPostprocessing = 0; 126 | }; 127 | 4E308A9D25915FBA0073C5D7 /* Frameworks */ = { 128 | isa = PBXFrameworksBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXFrameworksBuildPhase section */ 135 | 136 | /* Begin PBXGroup section */ 137 | 4E258F22259AC7D90093B65F /* Frameworks */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 4E258F23259AC7D90093B65F /* WidgetKit.framework */, 141 | 4E258F25259AC7D90093B65F /* SwiftUI.framework */, 142 | ); 143 | name = Frameworks; 144 | sourceTree = ""; 145 | }; 146 | 4E258F27259AC7D90093B65F /* MinecraftStatusWidgetViewExtension */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 4E4C387B25D8E4EB00D8C18B /* MinecraftStatusWidgetViewExtension.entitlements */, 150 | 4E3C08E4259C227C00303A35 /* Fonts */, 151 | 4E258F28259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.swift */, 152 | 4E258F2A259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition */, 153 | 4E258F2B259AC7DA0093B65F /* Assets.xcassets */, 154 | 4E258F2D259AC7DA0093B65F /* Info.plist */, 155 | 4E258F5A259BCD300093B65F /* PreviewData.swift */, 156 | ); 157 | path = MinecraftStatusWidgetViewExtension; 158 | sourceTree = ""; 159 | }; 160 | 4E308A7B25915FB80073C5D7 = { 161 | isa = PBXGroup; 162 | children = ( 163 | 4E4C387625D8E49C00D8C18B /* MinecraftStatus.entitlements */, 164 | 4E3C153A2591609B00549980 /* MinecraftStatusLib */, 165 | 4E308A8625915FB80073C5D7 /* MinecraftStatus */, 166 | 4E308A9825915FBA0073C5D7 /* MinecraftStatusTests */, 167 | 4E308AA325915FBA0073C5D7 /* MinecraftStatusUITests */, 168 | 4E258F27259AC7D90093B65F /* MinecraftStatusWidgetViewExtension */, 169 | 4E258F22259AC7D90093B65F /* Frameworks */, 170 | 4E308A8525915FB80073C5D7 /* Products */, 171 | ); 172 | sourceTree = ""; 173 | }; 174 | 4E308A8525915FB80073C5D7 /* Products */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 4E308A8425915FB80073C5D7 /* MinecraftStatus.app */, 178 | 4E308A9525915FBA0073C5D7 /* MinecraftStatusTests.xctest */, 179 | 4E308AA025915FBA0073C5D7 /* MinecraftStatusUITests.xctest */, 180 | 4E258F21259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.appex */, 181 | ); 182 | name = Products; 183 | sourceTree = ""; 184 | }; 185 | 4E308A8625915FB80073C5D7 /* MinecraftStatus */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 4E308A9025915FBA0073C5D7 /* Info.plist */, 189 | 4E308A8925915FB80073C5D7 /* SettingsView.swift */, 190 | 4E308A8725915FB80073C5D7 /* MinecraftStatusApp.swift */, 191 | 4E308A8B25915FBA0073C5D7 /* Assets.xcassets */, 192 | 4E308A8D25915FBA0073C5D7 /* Preview Content */, 193 | 4E6EC426259AAA58004B142B /* McStructs.swift */, 194 | ); 195 | path = MinecraftStatus; 196 | sourceTree = ""; 197 | }; 198 | 4E308A8D25915FBA0073C5D7 /* Preview Content */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 4E308A8E25915FBA0073C5D7 /* Preview Assets.xcassets */, 202 | ); 203 | path = "Preview Content"; 204 | sourceTree = ""; 205 | }; 206 | 4E308A9825915FBA0073C5D7 /* MinecraftStatusTests */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 4E308A9925915FBA0073C5D7 /* MinecraftStatusTests.swift */, 210 | 4E308A9B25915FBA0073C5D7 /* Info.plist */, 211 | ); 212 | path = MinecraftStatusTests; 213 | sourceTree = ""; 214 | }; 215 | 4E308AA325915FBA0073C5D7 /* MinecraftStatusUITests */ = { 216 | isa = PBXGroup; 217 | children = ( 218 | 4E308AA425915FBA0073C5D7 /* MinecraftStatusUITests.swift */, 219 | 4E308AA625915FBA0073C5D7 /* Info.plist */, 220 | ); 221 | path = MinecraftStatusUITests; 222 | sourceTree = ""; 223 | }; 224 | 4E3C08E4259C227C00303A35 /* Fonts */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | 4E3C08DD259C1DBB00303A35 /* minecraft.ttf */, 228 | 4E3C08E9259C23C300303A35 /* LICENSE */, 229 | ); 230 | path = Fonts; 231 | sourceTree = ""; 232 | }; 233 | 4E3C153A2591609B00549980 /* MinecraftStatusLib */ = { 234 | isa = PBXGroup; 235 | children = ( 236 | 4E3C153B259160C800549980 /* Minecraft-Status-Bridging-Header.h */, 237 | 4E1FCB3F259BF1350018AAA3 /* minecraft_status.h */, 238 | 4E1FCB34259BF07B0018AAA3 /* minecraft_status.a */, 239 | ); 240 | path = MinecraftStatusLib; 241 | sourceTree = ""; 242 | }; 243 | /* End PBXGroup section */ 244 | 245 | /* Begin PBXNativeTarget section */ 246 | 4E258F20259AC7D90093B65F /* MinecraftStatusWidgetViewExtension */ = { 247 | isa = PBXNativeTarget; 248 | buildConfigurationList = 4E258F36259AC7DA0093B65F /* Build configuration list for PBXNativeTarget "MinecraftStatusWidgetViewExtension" */; 249 | buildPhases = ( 250 | 4E1FCB23259BEE610018AAA3 /* ShellScript */, 251 | 4E258F1D259AC7D90093B65F /* Sources */, 252 | 4E38E85B25CF898500BF6025 /* ShellScript */, 253 | 4E258F1E259AC7D90093B65F /* Frameworks */, 254 | 4E258F1F259AC7D90093B65F /* Resources */, 255 | ); 256 | buildRules = ( 257 | ); 258 | dependencies = ( 259 | ); 260 | name = MinecraftStatusWidgetViewExtension; 261 | productName = MinecraftStatusWidgetViewExtension; 262 | productReference = 4E258F21259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.appex */; 263 | productType = "com.apple.product-type.app-extension"; 264 | }; 265 | 4E308A8325915FB80073C5D7 /* MinecraftStatus */ = { 266 | isa = PBXNativeTarget; 267 | buildConfigurationList = 4E308AA925915FBA0073C5D7 /* Build configuration list for PBXNativeTarget "MinecraftStatus" */; 268 | buildPhases = ( 269 | 4E308A8025915FB80073C5D7 /* Sources */, 270 | 4E308A8125915FB80073C5D7 /* Frameworks */, 271 | 4E308A8225915FB80073C5D7 /* Resources */, 272 | 4E258F33259AC7DA0093B65F /* Embed App Extensions */, 273 | ); 274 | buildRules = ( 275 | ); 276 | dependencies = ( 277 | 4E258F31259AC7DA0093B65F /* PBXTargetDependency */, 278 | ); 279 | name = MinecraftStatus; 280 | productName = MinecraftStatus; 281 | productReference = 4E308A8425915FB80073C5D7 /* MinecraftStatus.app */; 282 | productType = "com.apple.product-type.application"; 283 | }; 284 | 4E308A9425915FBA0073C5D7 /* MinecraftStatusTests */ = { 285 | isa = PBXNativeTarget; 286 | buildConfigurationList = 4E308AAC25915FBA0073C5D7 /* Build configuration list for PBXNativeTarget "MinecraftStatusTests" */; 287 | buildPhases = ( 288 | 4E308A9125915FBA0073C5D7 /* Sources */, 289 | 4E308A9225915FBA0073C5D7 /* Frameworks */, 290 | 4E308A9325915FBA0073C5D7 /* Resources */, 291 | ); 292 | buildRules = ( 293 | ); 294 | dependencies = ( 295 | 4E308A9725915FBA0073C5D7 /* PBXTargetDependency */, 296 | ); 297 | name = MinecraftStatusTests; 298 | productName = MinecraftStatusTests; 299 | productReference = 4E308A9525915FBA0073C5D7 /* MinecraftStatusTests.xctest */; 300 | productType = "com.apple.product-type.bundle.unit-test"; 301 | }; 302 | 4E308A9F25915FBA0073C5D7 /* MinecraftStatusUITests */ = { 303 | isa = PBXNativeTarget; 304 | buildConfigurationList = 4E308AAF25915FBA0073C5D7 /* Build configuration list for PBXNativeTarget "MinecraftStatusUITests" */; 305 | buildPhases = ( 306 | 4E308A9C25915FBA0073C5D7 /* Sources */, 307 | 4E308A9D25915FBA0073C5D7 /* Frameworks */, 308 | 4E308A9E25915FBA0073C5D7 /* Resources */, 309 | ); 310 | buildRules = ( 311 | ); 312 | dependencies = ( 313 | 4E308AA225915FBA0073C5D7 /* PBXTargetDependency */, 314 | ); 315 | name = MinecraftStatusUITests; 316 | productName = MinecraftStatusUITests; 317 | productReference = 4E308AA025915FBA0073C5D7 /* MinecraftStatusUITests.xctest */; 318 | productType = "com.apple.product-type.bundle.ui-testing"; 319 | }; 320 | /* End PBXNativeTarget section */ 321 | 322 | /* Begin PBXProject section */ 323 | 4E308A7C25915FB80073C5D7 /* Project object */ = { 324 | isa = PBXProject; 325 | attributes = { 326 | LastSwiftUpdateCheck = 1230; 327 | LastUpgradeCheck = 1230; 328 | TargetAttributes = { 329 | 4E258F20259AC7D90093B65F = { 330 | CreatedOnToolsVersion = 12.3; 331 | }; 332 | 4E308A8325915FB80073C5D7 = { 333 | CreatedOnToolsVersion = 12.3; 334 | LastSwiftMigration = 1230; 335 | }; 336 | 4E308A9425915FBA0073C5D7 = { 337 | CreatedOnToolsVersion = 12.3; 338 | TestTargetID = 4E308A8325915FB80073C5D7; 339 | }; 340 | 4E308A9F25915FBA0073C5D7 = { 341 | CreatedOnToolsVersion = 12.3; 342 | TestTargetID = 4E308A8325915FB80073C5D7; 343 | }; 344 | }; 345 | }; 346 | buildConfigurationList = 4E308A7F25915FB80073C5D7 /* Build configuration list for PBXProject "MinecraftStatus" */; 347 | compatibilityVersion = "Xcode 9.3"; 348 | developmentRegion = en; 349 | hasScannedForEncodings = 0; 350 | knownRegions = ( 351 | en, 352 | Base, 353 | ); 354 | mainGroup = 4E308A7B25915FB80073C5D7; 355 | productRefGroup = 4E308A8525915FB80073C5D7 /* Products */; 356 | projectDirPath = ""; 357 | projectRoot = ""; 358 | targets = ( 359 | 4E308A8325915FB80073C5D7 /* MinecraftStatus */, 360 | 4E308A9425915FBA0073C5D7 /* MinecraftStatusTests */, 361 | 4E308A9F25915FBA0073C5D7 /* MinecraftStatusUITests */, 362 | 4E258F20259AC7D90093B65F /* MinecraftStatusWidgetViewExtension */, 363 | ); 364 | }; 365 | /* End PBXProject section */ 366 | 367 | /* Begin PBXResourcesBuildPhase section */ 368 | 4E258F1F259AC7D90093B65F /* Resources */ = { 369 | isa = PBXResourcesBuildPhase; 370 | buildActionMask = 2147483647; 371 | files = ( 372 | 4E3C08DF259C1DBB00303A35 /* minecraft.ttf in Resources */, 373 | 4E258F2C259AC7DA0093B65F /* Assets.xcassets in Resources */, 374 | ); 375 | runOnlyForDeploymentPostprocessing = 0; 376 | }; 377 | 4E308A8225915FB80073C5D7 /* Resources */ = { 378 | isa = PBXResourcesBuildPhase; 379 | buildActionMask = 2147483647; 380 | files = ( 381 | 4E308A8F25915FBA0073C5D7 /* Preview Assets.xcassets in Resources */, 382 | 4E308A8C25915FBA0073C5D7 /* Assets.xcassets in Resources */, 383 | 4E3C08DE259C1DBB00303A35 /* minecraft.ttf in Resources */, 384 | ); 385 | runOnlyForDeploymentPostprocessing = 0; 386 | }; 387 | 4E308A9325915FBA0073C5D7 /* Resources */ = { 388 | isa = PBXResourcesBuildPhase; 389 | buildActionMask = 2147483647; 390 | files = ( 391 | ); 392 | runOnlyForDeploymentPostprocessing = 0; 393 | }; 394 | 4E308A9E25915FBA0073C5D7 /* Resources */ = { 395 | isa = PBXResourcesBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | ); 399 | runOnlyForDeploymentPostprocessing = 0; 400 | }; 401 | /* End PBXResourcesBuildPhase section */ 402 | 403 | /* Begin PBXShellScriptBuildPhase section */ 404 | 4E1FCB23259BEE610018AAA3 /* ShellScript */ = { 405 | isa = PBXShellScriptBuildPhase; 406 | buildActionMask = 2147483647; 407 | files = ( 408 | ); 409 | inputFileListPaths = ( 410 | ); 411 | inputPaths = ( 412 | ); 413 | outputFileListPaths = ( 414 | ); 415 | outputPaths = ( 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | shellPath = /bin/sh; 419 | shellScript = "# TODO: the rust stuff should be split out into its own framework\n$SRCROOT/scripts/generate-rust-lib.sh\n\n# cd's are needed to operate with the mintfile in context\ncd $SRCROOT/../\n$SRCROOT/scripts/bootstrap.sh\ncd -\n\n$SRCROOT/scripts/swiftformat.sh\n"; 420 | }; 421 | 4E38E85B25CF898500BF6025 /* ShellScript */ = { 422 | isa = PBXShellScriptBuildPhase; 423 | buildActionMask = 2147483647; 424 | files = ( 425 | ); 426 | inputFileListPaths = ( 427 | ); 428 | inputPaths = ( 429 | ); 430 | outputFileListPaths = ( 431 | ); 432 | outputPaths = ( 433 | ); 434 | runOnlyForDeploymentPostprocessing = 0; 435 | shellPath = /bin/sh; 436 | shellScript = "$SRCROOT/scripts/swiftlint.sh\n"; 437 | }; 438 | /* End PBXShellScriptBuildPhase section */ 439 | 440 | /* Begin PBXSourcesBuildPhase section */ 441 | 4E258F1D259AC7D90093B65F /* Sources */ = { 442 | isa = PBXSourcesBuildPhase; 443 | buildActionMask = 2147483647; 444 | files = ( 445 | 4E258F5B259BCD300093B65F /* PreviewData.swift in Sources */, 446 | 4E258F2E259AC7DA0093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition in Sources */, 447 | 4E258F51259ACBA90093B65F /* McStructs.swift in Sources */, 448 | 4E258F29259AC7D90093B65F /* MinecraftStatusWidgetViewExtension.swift in Sources */, 449 | ); 450 | runOnlyForDeploymentPostprocessing = 0; 451 | }; 452 | 4E308A8025915FB80073C5D7 /* Sources */ = { 453 | isa = PBXSourcesBuildPhase; 454 | buildActionMask = 2147483647; 455 | files = ( 456 | 4E308A8A25915FB80073C5D7 /* SettingsView.swift in Sources */, 457 | 4E6EC427259AAA58004B142B /* McStructs.swift in Sources */, 458 | 4E258F2F259AC7DA0093B65F /* MinecraftStatusWidgetViewExtension.intentdefinition in Sources */, 459 | 4E308A8825915FB80073C5D7 /* MinecraftStatusApp.swift in Sources */, 460 | ); 461 | runOnlyForDeploymentPostprocessing = 0; 462 | }; 463 | 4E308A9125915FBA0073C5D7 /* Sources */ = { 464 | isa = PBXSourcesBuildPhase; 465 | buildActionMask = 2147483647; 466 | files = ( 467 | 4E308A9A25915FBA0073C5D7 /* MinecraftStatusTests.swift in Sources */, 468 | ); 469 | runOnlyForDeploymentPostprocessing = 0; 470 | }; 471 | 4E308A9C25915FBA0073C5D7 /* Sources */ = { 472 | isa = PBXSourcesBuildPhase; 473 | buildActionMask = 2147483647; 474 | files = ( 475 | 4E308AA525915FBA0073C5D7 /* MinecraftStatusUITests.swift in Sources */, 476 | ); 477 | runOnlyForDeploymentPostprocessing = 0; 478 | }; 479 | /* End PBXSourcesBuildPhase section */ 480 | 481 | /* Begin PBXTargetDependency section */ 482 | 4E258F31259AC7DA0093B65F /* PBXTargetDependency */ = { 483 | isa = PBXTargetDependency; 484 | target = 4E258F20259AC7D90093B65F /* MinecraftStatusWidgetViewExtension */; 485 | targetProxy = 4E258F30259AC7DA0093B65F /* PBXContainerItemProxy */; 486 | }; 487 | 4E308A9725915FBA0073C5D7 /* PBXTargetDependency */ = { 488 | isa = PBXTargetDependency; 489 | target = 4E308A8325915FB80073C5D7 /* MinecraftStatus */; 490 | targetProxy = 4E308A9625915FBA0073C5D7 /* PBXContainerItemProxy */; 491 | }; 492 | 4E308AA225915FBA0073C5D7 /* PBXTargetDependency */ = { 493 | isa = PBXTargetDependency; 494 | target = 4E308A8325915FB80073C5D7 /* MinecraftStatus */; 495 | targetProxy = 4E308AA125915FBA0073C5D7 /* PBXContainerItemProxy */; 496 | }; 497 | /* End PBXTargetDependency section */ 498 | 499 | /* Begin XCBuildConfiguration section */ 500 | 4E258F34259AC7DA0093B65F /* Debug */ = { 501 | isa = XCBuildConfiguration; 502 | buildSettings = { 503 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 504 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 505 | CODE_SIGN_ENTITLEMENTS = MinecraftStatusWidgetViewExtension/MinecraftStatusWidgetViewExtension.entitlements; 506 | CODE_SIGN_STYLE = Automatic; 507 | CURRENT_PROJECT_VERSION = 2; 508 | DEVELOPMENT_TEAM = Z79T9AJ776; 509 | ENABLE_BITCODE = NO; 510 | INFOPLIST_FILE = MinecraftStatusWidgetViewExtension/Info.plist; 511 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 512 | LD_RUNPATH_SEARCH_PATHS = ( 513 | "$(inherited)", 514 | "@executable_path/Frameworks", 515 | "@executable_path/../../Frameworks", 516 | ); 517 | LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/MinecraftStatusLib"; 518 | MARKETING_VERSION = 1.0.0; 519 | PRODUCT_BUNDLE_IDENTIFIER = "dev.cldfire.minecraft-status.MinecraftStatusWidgetViewExtension"; 520 | PRODUCT_NAME = "$(TARGET_NAME)"; 521 | SKIP_INSTALL = YES; 522 | SWIFT_OBJC_BRIDGING_HEADER = "MinecraftStatusLib/Minecraft-Status-Bridging-Header.h"; 523 | SWIFT_VERSION = 5.0; 524 | TARGETED_DEVICE_FAMILY = "1,2"; 525 | }; 526 | name = Debug; 527 | }; 528 | 4E258F35259AC7DA0093B65F /* Release */ = { 529 | isa = XCBuildConfiguration; 530 | buildSettings = { 531 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 532 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 533 | CODE_SIGN_ENTITLEMENTS = MinecraftStatusWidgetViewExtension/MinecraftStatusWidgetViewExtension.entitlements; 534 | CODE_SIGN_STYLE = Automatic; 535 | CURRENT_PROJECT_VERSION = 2; 536 | DEVELOPMENT_TEAM = Z79T9AJ776; 537 | ENABLE_BITCODE = NO; 538 | INFOPLIST_FILE = MinecraftStatusWidgetViewExtension/Info.plist; 539 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 540 | LD_RUNPATH_SEARCH_PATHS = ( 541 | "$(inherited)", 542 | "@executable_path/Frameworks", 543 | "@executable_path/../../Frameworks", 544 | ); 545 | LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/MinecraftStatusLib"; 546 | MARKETING_VERSION = 1.0.0; 547 | PRODUCT_BUNDLE_IDENTIFIER = "dev.cldfire.minecraft-status.MinecraftStatusWidgetViewExtension"; 548 | PRODUCT_NAME = "$(TARGET_NAME)"; 549 | SKIP_INSTALL = YES; 550 | SWIFT_OBJC_BRIDGING_HEADER = "MinecraftStatusLib/Minecraft-Status-Bridging-Header.h"; 551 | SWIFT_VERSION = 5.0; 552 | TARGETED_DEVICE_FAMILY = "1,2"; 553 | }; 554 | name = Release; 555 | }; 556 | 4E308AA725915FBA0073C5D7 /* Debug */ = { 557 | isa = XCBuildConfiguration; 558 | buildSettings = { 559 | ALWAYS_SEARCH_USER_PATHS = NO; 560 | CLANG_ANALYZER_NONNULL = YES; 561 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 562 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 563 | CLANG_CXX_LIBRARY = "libc++"; 564 | CLANG_ENABLE_MODULES = YES; 565 | CLANG_ENABLE_OBJC_ARC = YES; 566 | CLANG_ENABLE_OBJC_WEAK = YES; 567 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 568 | CLANG_WARN_BOOL_CONVERSION = YES; 569 | CLANG_WARN_COMMA = YES; 570 | CLANG_WARN_CONSTANT_CONVERSION = YES; 571 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 572 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 573 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 574 | CLANG_WARN_EMPTY_BODY = YES; 575 | CLANG_WARN_ENUM_CONVERSION = YES; 576 | CLANG_WARN_INFINITE_RECURSION = YES; 577 | CLANG_WARN_INT_CONVERSION = YES; 578 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 579 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 580 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 581 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 582 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 583 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 584 | CLANG_WARN_STRICT_PROTOTYPES = YES; 585 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 586 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 587 | CLANG_WARN_UNREACHABLE_CODE = YES; 588 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 589 | COPY_PHASE_STRIP = NO; 590 | DEBUG_INFORMATION_FORMAT = dwarf; 591 | ENABLE_STRICT_OBJC_MSGSEND = YES; 592 | ENABLE_TESTABILITY = YES; 593 | GCC_C_LANGUAGE_STANDARD = gnu11; 594 | GCC_DYNAMIC_NO_PIC = NO; 595 | GCC_NO_COMMON_BLOCKS = YES; 596 | GCC_OPTIMIZATION_LEVEL = 0; 597 | GCC_PREPROCESSOR_DEFINITIONS = ( 598 | "DEBUG=1", 599 | "$(inherited)", 600 | ); 601 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 602 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 603 | GCC_WARN_UNDECLARED_SELECTOR = YES; 604 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 605 | GCC_WARN_UNUSED_FUNCTION = YES; 606 | GCC_WARN_UNUSED_VARIABLE = YES; 607 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 608 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 609 | MTL_FAST_MATH = YES; 610 | ONLY_ACTIVE_ARCH = YES; 611 | SDKROOT = iphoneos; 612 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 613 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 614 | }; 615 | name = Debug; 616 | }; 617 | 4E308AA825915FBA0073C5D7 /* Release */ = { 618 | isa = XCBuildConfiguration; 619 | buildSettings = { 620 | ALWAYS_SEARCH_USER_PATHS = NO; 621 | CLANG_ANALYZER_NONNULL = YES; 622 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 623 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 624 | CLANG_CXX_LIBRARY = "libc++"; 625 | CLANG_ENABLE_MODULES = YES; 626 | CLANG_ENABLE_OBJC_ARC = YES; 627 | CLANG_ENABLE_OBJC_WEAK = YES; 628 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 629 | CLANG_WARN_BOOL_CONVERSION = YES; 630 | CLANG_WARN_COMMA = YES; 631 | CLANG_WARN_CONSTANT_CONVERSION = YES; 632 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 633 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 634 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 635 | CLANG_WARN_EMPTY_BODY = YES; 636 | CLANG_WARN_ENUM_CONVERSION = YES; 637 | CLANG_WARN_INFINITE_RECURSION = YES; 638 | CLANG_WARN_INT_CONVERSION = YES; 639 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 640 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 641 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 642 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 643 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 644 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 645 | CLANG_WARN_STRICT_PROTOTYPES = YES; 646 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 647 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 648 | CLANG_WARN_UNREACHABLE_CODE = YES; 649 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 650 | COPY_PHASE_STRIP = NO; 651 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 652 | ENABLE_NS_ASSERTIONS = NO; 653 | ENABLE_STRICT_OBJC_MSGSEND = YES; 654 | GCC_C_LANGUAGE_STANDARD = gnu11; 655 | GCC_NO_COMMON_BLOCKS = YES; 656 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 657 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 658 | GCC_WARN_UNDECLARED_SELECTOR = YES; 659 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 660 | GCC_WARN_UNUSED_FUNCTION = YES; 661 | GCC_WARN_UNUSED_VARIABLE = YES; 662 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 663 | MTL_ENABLE_DEBUG_INFO = NO; 664 | MTL_FAST_MATH = YES; 665 | SDKROOT = iphoneos; 666 | SWIFT_COMPILATION_MODE = wholemodule; 667 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 668 | VALIDATE_PRODUCT = YES; 669 | }; 670 | name = Release; 671 | }; 672 | 4E308AAA25915FBA0073C5D7 /* Debug */ = { 673 | isa = XCBuildConfiguration; 674 | buildSettings = { 675 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 676 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 677 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 678 | CLANG_ENABLE_MODULES = YES; 679 | CODE_SIGN_ENTITLEMENTS = MinecraftStatus.entitlements; 680 | CODE_SIGN_STYLE = Automatic; 681 | CURRENT_PROJECT_VERSION = 2; 682 | DEVELOPMENT_ASSET_PATHS = "\"MinecraftStatus/Preview Content\""; 683 | DEVELOPMENT_TEAM = Z79T9AJ776; 684 | ENABLE_BITCODE = NO; 685 | ENABLE_PREVIEWS = YES; 686 | INFOPLIST_FILE = MinecraftStatus/Info.plist; 687 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 688 | LD_RUNPATH_SEARCH_PATHS = ( 689 | "$(inherited)", 690 | "@executable_path/Frameworks", 691 | ); 692 | LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/MinecraftStatusLib"; 693 | MARKETING_VERSION = 1.0.0; 694 | PRODUCT_BUNDLE_IDENTIFIER = "dev.cldfire.minecraft-status"; 695 | PRODUCT_NAME = "$(TARGET_NAME)"; 696 | SWIFT_OBJC_BRIDGING_HEADER = "MinecraftStatusLib/Minecraft-Status-Bridging-Header.h"; 697 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 698 | SWIFT_VERSION = 5.0; 699 | TARGETED_DEVICE_FAMILY = "1,2"; 700 | }; 701 | name = Debug; 702 | }; 703 | 4E308AAB25915FBA0073C5D7 /* Release */ = { 704 | isa = XCBuildConfiguration; 705 | buildSettings = { 706 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 707 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 708 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 709 | CLANG_ENABLE_MODULES = YES; 710 | CODE_SIGN_ENTITLEMENTS = MinecraftStatus.entitlements; 711 | CODE_SIGN_STYLE = Automatic; 712 | CURRENT_PROJECT_VERSION = 2; 713 | DEVELOPMENT_ASSET_PATHS = "\"MinecraftStatus/Preview Content\""; 714 | DEVELOPMENT_TEAM = Z79T9AJ776; 715 | ENABLE_BITCODE = NO; 716 | ENABLE_PREVIEWS = YES; 717 | INFOPLIST_FILE = MinecraftStatus/Info.plist; 718 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 719 | LD_RUNPATH_SEARCH_PATHS = ( 720 | "$(inherited)", 721 | "@executable_path/Frameworks", 722 | ); 723 | LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/MinecraftStatusLib"; 724 | MARKETING_VERSION = 1.0.0; 725 | PRODUCT_BUNDLE_IDENTIFIER = "dev.cldfire.minecraft-status"; 726 | PRODUCT_NAME = "$(TARGET_NAME)"; 727 | SWIFT_OBJC_BRIDGING_HEADER = "MinecraftStatusLib/Minecraft-Status-Bridging-Header.h"; 728 | SWIFT_VERSION = 5.0; 729 | TARGETED_DEVICE_FAMILY = "1,2"; 730 | }; 731 | name = Release; 732 | }; 733 | 4E308AAD25915FBA0073C5D7 /* Debug */ = { 734 | isa = XCBuildConfiguration; 735 | buildSettings = { 736 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 737 | BUNDLE_LOADER = "$(TEST_HOST)"; 738 | CODE_SIGN_STYLE = Automatic; 739 | DEVELOPMENT_TEAM = Z79T9AJ776; 740 | INFOPLIST_FILE = MinecraftStatusTests/Info.plist; 741 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 742 | LD_RUNPATH_SEARCH_PATHS = ( 743 | "$(inherited)", 744 | "@executable_path/Frameworks", 745 | "@loader_path/Frameworks", 746 | ); 747 | PRODUCT_BUNDLE_IDENTIFIER = dev.cldfire.MinecraftStatusTests; 748 | PRODUCT_NAME = "$(TARGET_NAME)"; 749 | SWIFT_VERSION = 5.0; 750 | TARGETED_DEVICE_FAMILY = "1,2"; 751 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MinecraftStatus.app/MinecraftStatus"; 752 | }; 753 | name = Debug; 754 | }; 755 | 4E308AAE25915FBA0073C5D7 /* Release */ = { 756 | isa = XCBuildConfiguration; 757 | buildSettings = { 758 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 759 | BUNDLE_LOADER = "$(TEST_HOST)"; 760 | CODE_SIGN_STYLE = Automatic; 761 | DEVELOPMENT_TEAM = Z79T9AJ776; 762 | INFOPLIST_FILE = MinecraftStatusTests/Info.plist; 763 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 764 | LD_RUNPATH_SEARCH_PATHS = ( 765 | "$(inherited)", 766 | "@executable_path/Frameworks", 767 | "@loader_path/Frameworks", 768 | ); 769 | PRODUCT_BUNDLE_IDENTIFIER = dev.cldfire.MinecraftStatusTests; 770 | PRODUCT_NAME = "$(TARGET_NAME)"; 771 | SWIFT_VERSION = 5.0; 772 | TARGETED_DEVICE_FAMILY = "1,2"; 773 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MinecraftStatus.app/MinecraftStatus"; 774 | }; 775 | name = Release; 776 | }; 777 | 4E308AB025915FBA0073C5D7 /* Debug */ = { 778 | isa = XCBuildConfiguration; 779 | buildSettings = { 780 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 781 | CODE_SIGN_STYLE = Automatic; 782 | DEVELOPMENT_TEAM = Z79T9AJ776; 783 | INFOPLIST_FILE = MinecraftStatusUITests/Info.plist; 784 | LD_RUNPATH_SEARCH_PATHS = ( 785 | "$(inherited)", 786 | "@executable_path/Frameworks", 787 | "@loader_path/Frameworks", 788 | ); 789 | PRODUCT_BUNDLE_IDENTIFIER = dev.cldfire.MinecraftStatusUITests; 790 | PRODUCT_NAME = "$(TARGET_NAME)"; 791 | SWIFT_VERSION = 5.0; 792 | TARGETED_DEVICE_FAMILY = "1,2"; 793 | TEST_TARGET_NAME = MinecraftStatus; 794 | }; 795 | name = Debug; 796 | }; 797 | 4E308AB125915FBA0073C5D7 /* Release */ = { 798 | isa = XCBuildConfiguration; 799 | buildSettings = { 800 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 801 | CODE_SIGN_STYLE = Automatic; 802 | DEVELOPMENT_TEAM = Z79T9AJ776; 803 | INFOPLIST_FILE = MinecraftStatusUITests/Info.plist; 804 | LD_RUNPATH_SEARCH_PATHS = ( 805 | "$(inherited)", 806 | "@executable_path/Frameworks", 807 | "@loader_path/Frameworks", 808 | ); 809 | PRODUCT_BUNDLE_IDENTIFIER = dev.cldfire.MinecraftStatusUITests; 810 | PRODUCT_NAME = "$(TARGET_NAME)"; 811 | SWIFT_VERSION = 5.0; 812 | TARGETED_DEVICE_FAMILY = "1,2"; 813 | TEST_TARGET_NAME = MinecraftStatus; 814 | }; 815 | name = Release; 816 | }; 817 | /* End XCBuildConfiguration section */ 818 | 819 | /* Begin XCConfigurationList section */ 820 | 4E258F36259AC7DA0093B65F /* Build configuration list for PBXNativeTarget "MinecraftStatusWidgetViewExtension" */ = { 821 | isa = XCConfigurationList; 822 | buildConfigurations = ( 823 | 4E258F34259AC7DA0093B65F /* Debug */, 824 | 4E258F35259AC7DA0093B65F /* Release */, 825 | ); 826 | defaultConfigurationIsVisible = 0; 827 | defaultConfigurationName = Release; 828 | }; 829 | 4E308A7F25915FB80073C5D7 /* Build configuration list for PBXProject "MinecraftStatus" */ = { 830 | isa = XCConfigurationList; 831 | buildConfigurations = ( 832 | 4E308AA725915FBA0073C5D7 /* Debug */, 833 | 4E308AA825915FBA0073C5D7 /* Release */, 834 | ); 835 | defaultConfigurationIsVisible = 0; 836 | defaultConfigurationName = Release; 837 | }; 838 | 4E308AA925915FBA0073C5D7 /* Build configuration list for PBXNativeTarget "MinecraftStatus" */ = { 839 | isa = XCConfigurationList; 840 | buildConfigurations = ( 841 | 4E308AAA25915FBA0073C5D7 /* Debug */, 842 | 4E308AAB25915FBA0073C5D7 /* Release */, 843 | ); 844 | defaultConfigurationIsVisible = 0; 845 | defaultConfigurationName = Release; 846 | }; 847 | 4E308AAC25915FBA0073C5D7 /* Build configuration list for PBXNativeTarget "MinecraftStatusTests" */ = { 848 | isa = XCConfigurationList; 849 | buildConfigurations = ( 850 | 4E308AAD25915FBA0073C5D7 /* Debug */, 851 | 4E308AAE25915FBA0073C5D7 /* Release */, 852 | ); 853 | defaultConfigurationIsVisible = 0; 854 | defaultConfigurationName = Release; 855 | }; 856 | 4E308AAF25915FBA0073C5D7 /* Build configuration list for PBXNativeTarget "MinecraftStatusUITests" */ = { 857 | isa = XCConfigurationList; 858 | buildConfigurations = ( 859 | 4E308AB025915FBA0073C5D7 /* Debug */, 860 | 4E308AB125915FBA0073C5D7 /* Release */, 861 | ); 862 | defaultConfigurationIsVisible = 0; 863 | defaultConfigurationName = Release; 864 | }; 865 | /* End XCConfigurationList section */ 866 | }; 867 | rootObject = 4E308A7C25915FB80073C5D7 /* Project object */; 868 | } 869 | --------------------------------------------------------------------------------