소스 검색

Show player count on landing page.

The server status needs to be stored in the redux store as it needs to be determined before
mounting the Landing component.
Daniel Scalzi 5 년 전
부모
커밋
67e42ead78

+ 42 - 21
package-lock.json

@@ -1362,14 +1362,20 @@
       }
       }
     },
     },
     "@eslint/eslintrc": {
     "@eslint/eslintrc": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.0.tgz",
-      "integrity": "sha512-bfL5365QSCmH6cPeFT7Ywclj8C7LiF7sO6mUGzZhtAMV7iID1Euq6740u/SRi4C80NOnVz/CEfK8/HO+nCAPJg==",
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz",
+      "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "ajv": "^6.12.4",
         "ajv": "^6.12.4",
         "debug": "^4.1.1",
         "debug": "^4.1.1",
+        "espree": "^7.3.0",
+        "globals": "^12.1.0",
+        "ignore": "^4.0.6",
         "import-fresh": "^3.2.1",
         "import-fresh": "^3.2.1",
+        "js-yaml": "^3.13.1",
+        "lodash": "^4.17.19",
+        "minimatch": "^3.0.4",
         "strip-json-comments": "^3.1.1"
         "strip-json-comments": "^3.1.1"
       },
       },
       "dependencies": {
       "dependencies": {
@@ -1385,11 +1391,26 @@
             "uri-js": "^4.2.2"
             "uri-js": "^4.2.2"
           }
           }
         },
         },
+        "globals": {
+          "version": "12.4.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+          "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+          "dev": true,
+          "requires": {
+            "type-fest": "^0.8.1"
+          }
+        },
         "strip-json-comments": {
         "strip-json-comments": {
           "version": "3.1.1",
           "version": "3.1.1",
           "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
           "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
           "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
           "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
           "dev": true
           "dev": true
+        },
+        "type-fest": {
+          "version": "0.8.1",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+          "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+          "dev": true
         }
         }
       }
       }
     },
     },
@@ -1577,9 +1598,9 @@
       "dev": true
       "dev": true
     },
     },
     "@types/node": {
     "@types/node": {
-      "version": "12.12.54",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.54.tgz",
-      "integrity": "sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w=="
+      "version": "12.12.55",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.55.tgz",
+      "integrity": "sha512-Vd6xQUVvPCTm7Nx1N7XHcpX6t047ltm7TgcsOr4gFHjeYgwZevo+V7I1lfzHnj5BT5frztZ42+RTG4MwYw63dw=="
     },
     },
     "@types/prop-types": {
     "@types/prop-types": {
       "version": "15.7.3",
       "version": "15.7.3",
@@ -4722,13 +4743,13 @@
       "dev": true
       "dev": true
     },
     },
     "eslint": {
     "eslint": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.0.tgz",
-      "integrity": "sha512-qgtVyLZqKd2ZXWnLQA4NtVbOyH56zivOAdBFWE54RFkSZjokzNrcP4Z0eVWsZ+84ByXv+jL9k/wE1ENYe8xRFw==",
+      "version": "7.8.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz",
+      "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "@babel/code-frame": "^7.0.0",
         "@babel/code-frame": "^7.0.0",
-        "@eslint/eslintrc": "^0.1.0",
+        "@eslint/eslintrc": "^0.1.3",
         "ajv": "^6.10.0",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
         "cross-spawn": "^7.0.2",
@@ -6072,18 +6093,18 @@
       }
       }
     },
     },
     "got": {
     "got": {
-      "version": "11.5.2",
-      "resolved": "https://registry.npmjs.org/got/-/got-11.5.2.tgz",
-      "integrity": "sha512-yUhpEDLeuGiGJjRSzEq3kvt4zJtAcjKmhIiwNp/eUs75tRlXfWcHo5tcBaMQtnjHWC7nQYT5HkY/l0QOQTkVww==",
+      "version": "11.6.0",
+      "resolved": "https://registry.npmjs.org/got/-/got-11.6.0.tgz",
+      "integrity": "sha512-ErhWb4IUjQzJ3vGs3+RR12NWlBDDkRciFpAkQ1LPUxi6OnwhGj07gQxjPsyIk69s7qMihwKrKquV6VQq7JNYLA==",
       "requires": {
       "requires": {
-        "@sindresorhus/is": "^3.0.0",
+        "@sindresorhus/is": "^3.1.1",
         "@szmarczak/http-timer": "^4.0.5",
         "@szmarczak/http-timer": "^4.0.5",
         "@types/cacheable-request": "^6.0.1",
         "@types/cacheable-request": "^6.0.1",
         "@types/responselike": "^1.0.0",
         "@types/responselike": "^1.0.0",
         "cacheable-lookup": "^5.0.3",
         "cacheable-lookup": "^5.0.3",
         "cacheable-request": "^7.0.1",
         "cacheable-request": "^7.0.1",
         "decompress-response": "^6.0.0",
         "decompress-response": "^6.0.0",
-        "http2-wrapper": "^1.0.0-beta.5.0",
+        "http2-wrapper": "^1.0.0-beta.5.2",
         "lowercase-keys": "^2.0.0",
         "lowercase-keys": "^2.0.0",
         "p-cancelable": "^2.0.0",
         "p-cancelable": "^2.0.0",
         "responselike": "^2.0.0"
         "responselike": "^2.0.0"
@@ -11440,9 +11461,9 @@
       }
       }
     },
     },
     "ts-node": {
     "ts-node": {
-      "version": "8.10.2",
-      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
-      "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
+      "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "arg": "^4.1.0",
         "arg": "^4.1.0",
@@ -11564,9 +11585,9 @@
       }
       }
     },
     },
     "typescript": {
     "typescript": {
-      "version": "3.9.7",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
-      "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz",
+      "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==",
       "dev": true
       "dev": true
     },
     },
     "unicode-canonical-property-names-ecmascript": {
     "unicode-canonical-property-names-ecmascript": {

+ 5 - 5
package.json

@@ -35,7 +35,7 @@
     "electron-updater": "^4.3.4",
     "electron-updater": "^4.3.4",
     "fs-extra": "^9.0.1",
     "fs-extra": "^9.0.1",
     "github-syntax-dark": "^0.5.0",
     "github-syntax-dark": "^0.5.0",
-    "got": "^11.5.2",
+    "got": "^11.6.0",
     "jquery": "^3.5.1",
     "jquery": "^3.5.1",
     "lodash": "^4.17.20",
     "lodash": "^4.17.20",
     "moment": "^2.27.0",
     "moment": "^2.27.0",
@@ -58,7 +58,7 @@
     "@types/jquery": "^3.5.1",
     "@types/jquery": "^3.5.1",
     "@types/lodash": "^4.14.161",
     "@types/lodash": "^4.14.161",
     "@types/mocha": "^8.0.3",
     "@types/mocha": "^8.0.3",
-    "@types/node": "^12.12.54",
+    "@types/node": "^12.12.55",
     "@types/react": "^16.9.49",
     "@types/react": "^16.9.49",
     "@types/react-dom": "^16.9.8",
     "@types/react-dom": "^16.9.8",
     "@types/react-redux": "^7.1.9",
     "@types/react-redux": "^7.1.9",
@@ -77,7 +77,7 @@
     "electron-devtools-installer": "^3.1.1",
     "electron-devtools-installer": "^3.1.1",
     "electron-webpack": "^2.8.2",
     "electron-webpack": "^2.8.2",
     "electron-webpack-ts": "^4.0.1",
     "electron-webpack-ts": "^4.0.1",
-    "eslint": "^7.8.0",
+    "eslint": "^7.8.1",
     "eslint-plugin-react": "^7.20.6",
     "eslint-plugin-react": "^7.20.6",
     "helios-distribution-types": "1.0.0-pre.1",
     "helios-distribution-types": "1.0.0-pre.1",
     "mocha": "^8.1.3",
     "mocha": "^8.1.3",
@@ -89,9 +89,9 @@
     "react-transition-group": "^4.4.1",
     "react-transition-group": "^4.4.1",
     "redux": "^4.0.5",
     "redux": "^4.0.5",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",
-    "ts-node": "^8.10.2",
+    "ts-node": "^9.0.0",
     "tsconfig-paths": "^3.9.0",
     "tsconfig-paths": "^3.9.0",
-    "typescript": "^3.9.7",
+    "typescript": "^4.0.2",
     "webpack": "^4.44.1"
     "webpack": "^4.44.1"
   },
   },
   "repository": {
   "repository": {

+ 21 - 0
src/common/distribution/DistributionFactory.ts

@@ -53,13 +53,34 @@ export class HeliosDistribution {
 export class HeliosServer {
 export class HeliosServer {
 
 
     public readonly modules: HeliosModule[]
     public readonly modules: HeliosModule[]
+    public readonly hostname: string
+    public readonly port: number
 
 
     constructor(
     constructor(
         public readonly rawServer: Server
         public readonly rawServer: Server
     ) {
     ) {
+        const { hostname, port } = this.parseAddress()
+        this.hostname = hostname
+        this.port = port
         this.modules = rawServer.modules.map(m => new HeliosModule(m, rawServer.id))
         this.modules = rawServer.modules.map(m => new HeliosModule(m, rawServer.id))
     }
     }
 
 
+    private parseAddress(): { hostname: string, port: number } {
+        // Srv record lookup here if needed.
+        if(this.rawServer.address.includes(':')) {
+            const pieces = this.rawServer.address.split(':')
+            const port = Number(pieces[1])
+
+            if(!Number.isInteger(port)) {
+                throw new Error(`Malformed server address for ${this.rawServer.id}. Port must be an integer!`)
+            }
+
+            return { hostname: pieces[0], port }
+        } else {
+            return { hostname: this.rawServer.address, port: 25565 }
+        }
+    }
+
 }
 }
 
 
 export class HeliosModule {
 export class HeliosModule {

+ 16 - 16
src/common/mojang/net/ServerStatusAPI.ts

@@ -35,17 +35,17 @@ export interface ServerStatus {
  * Get the handshake packet.
  * Get the handshake packet.
  * 
  * 
  * @param protocol The client's protocol version.
  * @param protocol The client's protocol version.
- * @param address The server address.
+ * @param hostname The server hostname.
  * @param port The server port.
  * @param port The server port.
  * 
  * 
  * @see https://wiki.vg/Server_List_Ping#Handshake
  * @see https://wiki.vg/Server_List_Ping#Handshake
  */
  */
-function getHandshakePacket(protocol: number, address: string, port: number): Buffer {
+function getHandshakePacket(protocol: number, hostname: string, port: number): Buffer {
 
 
     return ServerBoundPacket.build()
     return ServerBoundPacket.build()
         .writeVarInt(0x00)         // Packet Id 
         .writeVarInt(0x00)         // Packet Id 
         .writeVarInt(protocol)
         .writeVarInt(protocol)
-        .writeString(address)
+        .writeString(hostname)
         .writeUnsignedShort(port)
         .writeUnsignedShort(port)
         .writeVarInt(1)            // State, 1 = status
         .writeVarInt(1)            // State, 1 = status
         .toBuffer()
         .toBuffer()
@@ -80,19 +80,19 @@ function unifyStatusResponse(resp: ServerStatus): ServerStatus {
     return resp
     return resp
 }
 }
 
 
-export function getServerStatus(protocol: number, address: string, port = 25565): Promise<ServerStatus | null> {
+export function getServerStatus(protocol: number, hostname: string, port = 25565): Promise<ServerStatus | undefined> {
 
 
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
 
 
-        const socket = connect(port, address, () => {
-            socket.write(getHandshakePacket(protocol, address, port))
+        const socket = connect(port, hostname, () => {
+            socket.write(getHandshakePacket(protocol, hostname, port))
             socket.write(getRequestPacket())
             socket.write(getRequestPacket())
         })
         })
 
 
         socket.setTimeout(5000, () => {
         socket.setTimeout(5000, () => {
             socket.destroy()
             socket.destroy()
-            logger.error(`Server Status Socket timed out (${address}:${port})`)
-            reject(new Error(`Server Status Socket timed out (${address}:${port})`))
+            logger.error(`Server Status Socket timed out (${hostname}:${port})`)
+            reject(new Error(`Server Status Socket timed out (${hostname}:${port})`))
         })
         })
 
 
         const maxTries = 2
         const maxTries = 2
@@ -122,7 +122,7 @@ export function getServerStatus(protocol: number, address: string, port = 25565)
 
 
                 if(iterations > maxTries) {
                 if(iterations > maxTries) {
                     socket.destroy()
                     socket.destroy()
-                    reject(new Error(`Data read from ${address}:${port} exceeded ${maxTries} iterations, closing connection.`))
+                    reject(new Error(`Data read from ${hostname}:${port} exceeded ${maxTries} iterations, closing connection.`))
                     return
                     return
                 }
                 }
                 ++iterations
                 ++iterations
@@ -141,7 +141,7 @@ export function getServerStatus(protocol: number, address: string, port = 25565)
                     const result = inboundPacket.readString()
                     const result = inboundPacket.readString()
 
 
                     try {
                     try {
-                        const parsed = JSON.parse(result)
+                        const parsed: ServerStatus = JSON.parse(result)
                         socket.end()
                         socket.end()
                         resolve(unifyStatusResponse(parsed))
                         resolve(unifyStatusResponse(parsed))
                     } catch(err) {
                     } catch(err) {
@@ -164,17 +164,17 @@ export function getServerStatus(protocol: number, address: string, port = 25565)
 
 
             if(err.code === 'ENOTFOUND') {
             if(err.code === 'ENOTFOUND') {
                 // ENOTFOUND = Unable to resolve.
                 // ENOTFOUND = Unable to resolve.
-                logger.error(`Server ${address}:${port} not found!`)
-                resolve(null)
+                logger.error(`Server ${hostname}:${port} not found!`)
+                resolve(undefined)
                 return
                 return
             } else if(err.code === 'ECONNREFUSED') {
             } else if(err.code === 'ECONNREFUSED') {
                 // ECONNREFUSED = Unable to connect to port.
                 // ECONNREFUSED = Unable to connect to port.
-                logger.error(`Server ${address}:${port} refused to connect, is the port correct?`)
-                resolve(null)
+                logger.error(`Server ${hostname}:${port} refused to connect, is the port correct?`)
+                resolve(undefined)
                 return
                 return
             } else {
             } else {
-                logger.error(`Error trying to pull server status (${address}:${port})`, err)
-                resolve(null)
+                logger.error(`Error trying to pull server status (${hostname}:${port})`, err)
+                resolve(undefined)
                 return
                 return
             }
             }
         })
         })

+ 29 - 15
src/renderer/components/Application.tsx

@@ -21,7 +21,7 @@ import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overl
 
 
 import { LoggerUtil } from 'common/logging/loggerutil'
 import { LoggerUtil } from 'common/logging/loggerutil'
 import { DistributionAPI } from 'common/distribution/DistributionAPI'
 import { DistributionAPI } from 'common/distribution/DistributionAPI'
-import { getServerStatus } from 'common/mojang/net/ServerStatusAPI'
+import { getServerStatus, ServerStatus } from 'common/mojang/net/ServerStatusAPI'
 import { Distribution } from 'helios-distribution-types'
 import { Distribution } from 'helios-distribution-types'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
 
 
@@ -40,6 +40,7 @@ interface ApplicationProps {
     overlayQueue: OverlayPushAction<unknown>[]
     overlayQueue: OverlayPushAction<unknown>[]
     distribution: HeliosDistribution
     distribution: HeliosDistribution
     selectedServer: HeliosServer
     selectedServer: HeliosServer
+    selectedServerStatus: ServerStatus
 }
 }
 
 
 interface ApplicationState {
 interface ApplicationState {
@@ -79,7 +80,7 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
         }
         }
     }
     }
 
 
-    getViewElement(): JSX.Element {
+    private getViewElement = (): JSX.Element => {
         // TODO debug remove
         // TODO debug remove
         console.log('loading', this.props.currentView, this.state.workingView)
         console.log('loading', this.props.currentView, this.state.workingView)
         switch(this.state.workingView) {
         switch(this.state.workingView) {
@@ -89,7 +90,11 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
                 </>
                 </>
             case View.LANDING:
             case View.LANDING:
                 return <>
                 return <>
-                    <Landing distribution={this.props.distribution} selectedServer={this.props.selectedServer} />
+                    <Landing
+                        distribution={this.props.distribution}
+                        selectedServer={this.props.selectedServer}
+                        selectedServerStatus={this.props.selectedServerStatus}
+                    />
                 </>
                 </>
             case View.LOGIN:
             case View.LOGIN:
                 return <>
                 return <>
@@ -164,10 +169,19 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
                 return
                 return
             } else {
             } else {
                 const distro = new HeliosDistribution(rawDisto)
                 const distro = new HeliosDistribution(rawDisto)
-                this.props.setDistribution(distro)
                 // TODO TEMP USE CONFIG
                 // TODO TEMP USE CONFIG
                 // TODO TODO TODO TODO
                 // TODO TODO TODO TODO
-                this.props.setSelectedServer(distro.servers[0])
+                const selectedServer: HeliosServer = distro.servers[0]
+                const { hostname, port } = selectedServer
+                let selectedServerStatus
+                try {
+                    selectedServerStatus = await getServerStatus(47, hostname, port)
+                } catch(err) {
+                    Application.logger.error('Failed to refresh server status', selectedServerStatus)
+                }
+                this.props.setDistribution(distro)
+                this.props.setSelectedServer(selectedServer)
+                this.props.setSelectedServerStatus(selectedServerStatus)
             }
             }
 
 
             // TODO Setup hook for distro refresh every ~ 5 mins.
             // TODO Setup hook for distro refresh every ~ 5 mins.
@@ -186,16 +200,16 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
                 })
                 })
                 // TODO temp
                 // TODO temp
                 setTimeout(() => {
                 setTimeout(() => {
-                    //this.props.setView(View.WELCOME)
-                    this.props.pushGenericOverlay({
-                        title: 'Load Distribution',
-                        description: 'This is a test.',
-                        dismissible: false,
-                        acknowledgeCallback: async () => {
-                            const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565)
-                            console.log(serverStatus)
-                        }
-                    })
+                    // this.props.setView(View.WELCOME)
+                    // this.props.pushGenericOverlay({
+                    //     title: 'Load Distribution',
+                    //     description: 'This is a test.',
+                    //     dismissible: false,
+                    //     acknowledgeCallback: async () => {
+                    //         const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565)
+                    //         console.log(serverStatus)
+                    //     }
+                    // })
                     // this.props.pushGenericOverlay({
                     // this.props.pushGenericOverlay({
                     //     title: 'Test Title 2',
                     //     title: 'Test Title 2',
                     //     description: 'Test Description',
                     //     description: 'Test Description',

+ 16 - 0
src/renderer/components/landing/Landing.css

@@ -1,3 +1,19 @@
+.serverStatusWrapper-enter {
+    opacity: 0;
+}
+.serverStatusWrapper-enter-active {
+    opacity: 1;
+    transition: opacity 500ms, transform 500ms;
+}
+.serverStatusWrapper-exit {
+    opacity: 1;
+}
+.serverStatusWrapper-exit-active {
+    opacity: 0;
+    transition: opacity 500ms, transform 500ms;
+}
+
+
 /*******************************************************************************
 /*******************************************************************************
  *                                                                             *
  *                                                                             *
  * Landing View (Structural Styles)                                            *
  * Landing View (Structural Styles)                                            *

+ 81 - 8
src/renderer/components/landing/Landing.tsx

@@ -1,10 +1,12 @@
 import * as React from 'react'
 import * as React from 'react'
 import { connect } from 'react-redux'
 import { connect } from 'react-redux'
+import { CSSTransition } from 'react-transition-group'
 
 
 import { StoreType } from '../../redux/store'
 import { StoreType } from '../../redux/store'
 import { AppActionDispatch } from '../..//redux/actions/appActions'
 import { AppActionDispatch } from '../..//redux/actions/appActions'
 import { OverlayActionDispatch } from '../../redux/actions/overlayActions'
 import { OverlayActionDispatch } from '../../redux/actions/overlayActions'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
+import { ServerStatus, getServerStatus } from 'common/mojang/net/ServerStatusAPI'
 import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
 import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
 import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
 import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
 import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
 import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
@@ -18,16 +20,19 @@ import './Landing.css'
 interface LandingProps {
 interface LandingProps {
     distribution: HeliosDistribution
     distribution: HeliosDistribution
     selectedServer: HeliosServer
     selectedServer: HeliosServer
+    selectedServerStatus: ServerStatus
 }
 }
 
 
 interface LandingState {
 interface LandingState {
     mojangStatuses: MojangStatus[]
     mojangStatuses: MojangStatus[]
+    outdatedServerStatus: boolean
 }
 }
 
 
 const mapState = (state: StoreType): Partial<LandingProps> => {
 const mapState = (state: StoreType): Partial<LandingProps> => {
     return {
     return {
         distribution: state.app.distribution!,
         distribution: state.app.distribution!,
-        selectedServer: state.app.selectedServer!
+        selectedServer: state.app.selectedServer!,
+        selectedServerStatus: state.app.selectedServerStatus!
     }
     }
 }
 }
 const mapDispatch = {
 const mapDispatch = {
@@ -42,11 +47,13 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
     private static readonly logger = LoggerUtil.getLogger('LandingTSX')
     private static readonly logger = LoggerUtil.getLogger('LandingTSX')
 
 
     private mojangStatusInterval!: NodeJS.Timeout
     private mojangStatusInterval!: NodeJS.Timeout
+    private serverStatusInterval!: NodeJS.Timeout
 
 
     constructor(props: InternalLandingProps) {
     constructor(props: InternalLandingProps) {
         super(props)
         super(props)
         this.state = {
         this.state = {
-            mojangStatuses: []
+            mojangStatuses: [],
+            outdatedServerStatus: false
         }
         }
     }
     }
 
 
@@ -60,12 +67,21 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
             await this.loadMojangStatuses()
             await this.loadMojangStatuses()
         }, 300000)
         }, 300000)
 
 
+        this.serverStatusInterval = setInterval(async () => {
+            Landing.logger.info('Refreshing selected server status..')
+            this.setState({
+                ...this.state,
+                outdatedServerStatus: true
+            })
+        }, 300000)
+
     }
     }
 
 
     componentWillUnmount(): void {
     componentWillUnmount(): void {
 
 
         // Clean up intervals.
         // Clean up intervals.
         clearInterval(this.mojangStatusInterval)
         clearInterval(this.mojangStatusInterval)
+        clearInterval(this.serverStatusInterval)
 
 
     }
     }
 
 
@@ -92,6 +108,34 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
 
 
     }
     }
 
 
+    private syncServerStatus = async (): Promise<void> => {
+        let serverStatus: ServerStatus | undefined
+
+        if(this.props.selectedServer != null) {
+            const { hostname, port } = this.props.selectedServer
+            try {
+                serverStatus = await getServerStatus(
+                    47,
+                    hostname,
+                    port
+                )
+            } catch(err) {
+                Landing.logger.error('Error while refreshing server status', err)
+            }
+            
+        } else {
+            serverStatus = undefined
+        }
+
+        this.props.setSelectedServerStatus(serverStatus)
+    }
+    private finishServerSync = async (): Promise<void> => {
+        this.setState({
+            ...this.state,
+            outdatedServerStatus: false
+        })
+    }
+
     private getMainMojangStatusColor = (): string => {
     private getMainMojangStatusColor = (): string => {
         const essential = this.state.mojangStatuses.filter(s => s.essential)
         const essential = this.state.mojangStatuses.filter(s => s.essential)
 
 
@@ -137,9 +181,14 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
         this.props.pushServerSelectOverlay({
         this.props.pushServerSelectOverlay({
             servers: this.props.distribution.servers,
             servers: this.props.distribution.servers,
             selectedId: this.props.selectedServer.rawServer.id,
             selectedId: this.props.selectedServer.rawServer.id,
-            onSelection: (serverId: string) => {
+            onSelection: async (serverId: string) => {
                 Landing.logger.info('Server Selection Change:', serverId)
                 Landing.logger.info('Server Selection Change:', serverId)
-                this.props.setSelectedServer(this.props.distribution.getServerById(serverId)!)
+                const next: HeliosServer = this.props.distribution.getServerById(serverId)!
+                this.props.setSelectedServer(next)
+                this.setState({
+                    ...this.state,
+                    outdatedServerStatus: true
+                })
             }
             }
         })
         })
     }
     }
@@ -152,6 +201,19 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
         }
         }
     }
     }
 
 
+    private getSelectedServerStatusText = (): string => {
+        return this.props.selectedServerStatus != null ? 'PLAYERS' : 'SERVER'
+    }
+
+    private getSelectedServerCount = (): string => {
+        if(this.props.selectedServerStatus != null) {
+            const { online, max } = this.props.selectedServerStatus.players
+            return `${online}/${max}`
+        } else {
+            return 'OFFLINE'
+        }
+    }
+
     render(): JSX.Element {
     render(): JSX.Element {
         return <>
         return <>
             
             
@@ -252,10 +314,21 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
                     <div id="left">
                     <div id="left">
                         <div className="bot_wrapper">
                         <div className="bot_wrapper">
                             <div id="content">
                             <div id="content">
-                                <div id="server_status_wrapper">
-                                    <span className="bot_label" id="landingPlayerLabel">SERVER</span>
-                                    <span id="player_count">OFFLINE</span>
-                                </div>
+                                
+                                <CSSTransition
+                                    in={!this.state.outdatedServerStatus}
+                                    timeout={500}
+                                    classNames="serverStatusWrapper"
+                                    unmountOnExit
+                                    onEnter={this.syncServerStatus}
+                                    onExited={this.finishServerSync}
+                                >
+                                    <div id="server_status_wrapper">
+                                        <span className="bot_label" id="landingPlayerLabel">{this.getSelectedServerStatusText()}</span>
+                                        <span id="player_count">{this.getSelectedServerCount()}</span>
+                                    </div>
+                                </CSSTransition>
+                                
                                 <div className="bot_divider"></div>
                                 <div className="bot_divider"></div>
                                 <div id="mojangStatusWrapper">
                                 <div id="mojangStatusWrapper">
                                     <span className="bot_label">MOJANG STATUS</span>
                                     <span className="bot_label">MOJANG STATUS</span>

+ 2 - 2
src/renderer/components/overlay/server-select/ServerSelectOverlay.tsx

@@ -10,7 +10,7 @@ import '../shared-select/SharedSelect.css'
 export interface ServerSelectOverlayProps {
 export interface ServerSelectOverlayProps {
     servers: HeliosServer[]
     servers: HeliosServer[]
     selectedId: string
     selectedId: string
-    onSelection: (serverId: string) => void
+    onSelection: (serverId: string) => Promise<void>
 }
 }
 
 
 interface ServerSelectOverlayState {
 interface ServerSelectOverlayState {
@@ -36,7 +36,7 @@ class ServerSelectOverlay extends React.Component<InternalServerSelectOverlayPro
 
 
     private onSelectClick = async (): Promise<void> => {
     private onSelectClick = async (): Promise<void> => {
         try {
         try {
-            this.props.onSelection(this.state.selectedId)
+            await this.props.onSelection(this.state.selectedId)
         } catch(err) {
         } catch(err) {
             this.logger.error('Uncaught error in server select confirmation.', err)
             this.logger.error('Uncaught error in server select confirmation.', err)
         }
         }

+ 1 - 0
src/renderer/index.tsx

@@ -30,6 +30,7 @@ ReactDOM.render(
                 overlayQueue={store.getState().overlayQueue}
                 overlayQueue={store.getState().overlayQueue}
                 distribution={store.getState().app.distribution!}
                 distribution={store.getState().app.distribution!}
                 selectedServer={store.getState().app.selectedServer!}
                 selectedServer={store.getState().app.selectedServer!}
+                selectedServerStatus={store.getState().app.selectedServerStatus!}
             />
             />
         </Provider>
         </Provider>
     </AppContainer>,
     </AppContainer>,

+ 21 - 7
src/renderer/redux/actions/appActions.ts

@@ -1,37 +1,51 @@
 import { Action } from 'redux'
 import { Action } from 'redux'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
+import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
 
 
 export enum AppActionType {
 export enum AppActionType {
     SetDistribution = 'SET_DISTRIBUTION',
     SetDistribution = 'SET_DISTRIBUTION',
-    SetSelectedServer = 'SET_SELECTED_SERVER'
+    SetSelectedServer = 'SET_SELECTED_SERVER',
+    SetSelectedServerStatus = 'SET_SELECTED_SERVER_STATUS'
 }
 }
 
 
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface AppAction extends Action {}
 export interface AppAction extends Action {}
 
 
 export interface SetDistributionAction extends AppAction {
 export interface SetDistributionAction extends AppAction {
-    payload: HeliosDistribution
+    payload?: HeliosDistribution
 }
 }
 
 
 export interface SetSelectedServerAction extends AppAction {
 export interface SetSelectedServerAction extends AppAction {
-    payload: HeliosServer
+    payload?: HeliosServer
 }
 }
 
 
-export function setDistribution(distribution: HeliosDistribution): SetDistributionAction {
+export interface SetSelectedServerStatusAction extends AppAction {
+    payload?: ServerStatus
+}
+
+export function setDistribution(distribution?: HeliosDistribution): SetDistributionAction {
     return {
     return {
         type: AppActionType.SetDistribution,
         type: AppActionType.SetDistribution,
         payload: distribution
         payload: distribution
     }
     }
 }
 }
 
 
-export function setSelectedServer(server: HeliosServer): SetSelectedServerAction {
+export function setSelectedServer(server?: HeliosServer): SetSelectedServerAction {
     return {
     return {
         type: AppActionType.SetSelectedServer,
         type: AppActionType.SetSelectedServer,
         payload: server
         payload: server
     }
     }
 }
 }
 
 
+export function setSelectedServerStatus(serverStatus?: ServerStatus): SetSelectedServerStatusAction {
+    return {
+        type: AppActionType.SetSelectedServerStatus,
+        payload: serverStatus
+    }
+}
+
 export const AppActionDispatch = {
 export const AppActionDispatch = {
-    setDistribution: (d: HeliosDistribution): SetDistributionAction => setDistribution(d),
-    setSelectedServer: (s: HeliosServer): SetSelectedServerAction => setSelectedServer(s)
+    setDistribution: (d?: HeliosDistribution): SetDistributionAction => setDistribution(d),
+    setSelectedServer: (s?: HeliosServer): SetSelectedServerAction => setSelectedServer(s),
+    setSelectedServerStatus: (ss?: ServerStatus): SetSelectedServerStatusAction => setSelectedServerStatus(ss)
 }
 }

+ 13 - 5
src/renderer/redux/reducers/appReducer.ts

@@ -1,15 +1,18 @@
-import { AppActionType, AppAction, SetDistributionAction, SetSelectedServerAction } from '../actions/appActions'
+import { AppActionType, AppAction, SetDistributionAction, SetSelectedServerAction, SetSelectedServerStatusAction } from '../actions/appActions'
 import { Reducer } from 'redux'
 import { Reducer } from 'redux'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
 import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
+import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
 
 
 export interface AppState {
 export interface AppState {
-    distribution: HeliosDistribution | null
-    selectedServer: HeliosServer | null
+    distribution?: HeliosDistribution
+    selectedServer?: HeliosServer
+    selectedServerStatus?: ServerStatus
 }
 }
 
 
 const defaultAppState: AppState = {
 const defaultAppState: AppState = {
-    distribution: null,
-    selectedServer: null
+    distribution: undefined,
+    selectedServer: undefined,
+    selectedServerStatus: undefined
 }
 }
 
 
 const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => {
 const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => {
@@ -24,6 +27,11 @@ const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, actio
                 ...state,
                 ...state,
                 selectedServer: (action as SetSelectedServerAction).payload
                 selectedServer: (action as SetSelectedServerAction).payload
             }
             }
+        case AppActionType.SetSelectedServerStatus:
+            return {
+                ...state,
+                selectedServerStatus: (action as SetSelectedServerStatusAction).payload
+            }
     }
     }
     return state
     return state
 }
 }