Early version of multiplayer
This commit is contained in:
4 files changed
+491
-9
No files matched your search
@@ -3,7 +3,7 @@
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Host: localhost
|
||||
-- Generation Time: Feb 21, 2026 at 09:07 PM
|
||||
-- Generation Time: Mar 05, 2026 at 05:31 AM
|
||||
-- Server version: 11.8.6-MariaDB-log
|
||||
-- PHP Version: 8.5.3
|
||||
|
||||
@@ -70,6 +70,20 @@ CREATE TABLE `marketplaceicons` (
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `multiplayer_player_joins_and_leaves`
|
||||
--
|
||||
|
||||
CREATE TABLE `multiplayer_player_joins_and_leaves` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`wsId` varchar(32) NOT NULL,
|
||||
`pubId` varchar(32) NOT NULL,
|
||||
`type` enum('Join','Leave') NOT NULL,
|
||||
`timestamp` bigint(20) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPRESSED;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `splashtexts`
|
||||
--
|
||||
@@ -119,7 +133,7 @@ CREATE TABLE `userposts` (
|
||||
--
|
||||
ALTER TABLE `chatroom_reports`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD KEY `chatId` (`chatId`);
|
||||
ADD KEY `chatroom_reports?chatId` (`chatId`);
|
||||
|
||||
--
|
||||
-- Indexes for table `chats`
|
||||
@@ -133,6 +147,12 @@ ALTER TABLE `chats`
|
||||
ALTER TABLE `marketplaceicons`
|
||||
ADD PRIMARY KEY (`place`);
|
||||
|
||||
--
|
||||
-- Indexes for table `multiplayer_player_joins_and_leaves`
|
||||
--
|
||||
ALTER TABLE `multiplayer_player_joins_and_leaves`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indexes for table `splashtexts`
|
||||
--
|
||||
@@ -173,6 +193,12 @@ ALTER TABLE `chats`
|
||||
ALTER TABLE `marketplaceicons`
|
||||
MODIFY `place` bigint(20) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `multiplayer_player_joins_and_leaves`
|
||||
--
|
||||
ALTER TABLE `multiplayer_player_joins_and_leaves`
|
||||
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `splashtexts`
|
||||
--
|
||||
@@ -199,7 +225,7 @@ ALTER TABLE `userposts`
|
||||
-- Constraints for table `chatroom_reports`
|
||||
--
|
||||
ALTER TABLE `chatroom_reports`
|
||||
ADD CONSTRAINT `chatId` FOREIGN KEY (`chatId`) REFERENCES `chats` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ADD CONSTRAINT `chatroom_reports?chatId` FOREIGN KEY (`chatId`) REFERENCES `chats` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
|
||||
+356
-6
@@ -7,6 +7,7 @@ import swagger from '@elysiajs/swagger'
|
||||
import { berryDashChats, berryDashUserData, users } from './lib/tables'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { checkAuthorization } from './lib/auth'
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
import { handler as getVerifyCodeHandler } from './routes/get-verify-code'
|
||||
|
||||
@@ -52,6 +53,9 @@ import { handler as berryDashSplashTextPostHandler } from './routes/berrydash/sp
|
||||
|
||||
import { handler as berryDashRenderIconGetHandler } from './routes/berrydash/render-icon/get'
|
||||
|
||||
import { handler as websiteLauncherGamesGetHandler } from './routes/website/launcher/games/get'
|
||||
import { handler as websiteLauncherGameGetHandler } from './routes/website/launcher/game/get'
|
||||
|
||||
dotenv.config({ quiet: true })
|
||||
|
||||
const intNotStr = (name: string) => {
|
||||
@@ -106,19 +110,315 @@ interface ClientInfoData {
|
||||
ClientPlatform: string
|
||||
}
|
||||
|
||||
interface MultiplayerClient {
|
||||
Id: string
|
||||
Key: string
|
||||
PosX: number
|
||||
PosY: number
|
||||
Flipped: boolean
|
||||
Paused: boolean
|
||||
FlaggedReason: string | null
|
||||
Token: string
|
||||
Username: string
|
||||
UserID: number
|
||||
Admin: boolean
|
||||
Platform: number | null
|
||||
JoinTimestamp: number
|
||||
}
|
||||
|
||||
interface MultiplayerBerry {
|
||||
Id: string
|
||||
Type: string
|
||||
Timestamp: number
|
||||
PosX: number
|
||||
}
|
||||
|
||||
const clients = new Set<ElysiaWS>()
|
||||
const clientInfo: ClientInfoData[] = []
|
||||
let clientInfo: ClientInfoData[] = []
|
||||
let multiplayerClients: MultiplayerClient[] = []
|
||||
let multiplayerBerries: MultiplayerBerry[] = []
|
||||
|
||||
setInterval(async () => {
|
||||
if (multiplayerClients.length == 0) return
|
||||
let berry = ''
|
||||
const spawnProbability = Math.random()
|
||||
const id = randomBytes(16).toString('hex')
|
||||
if (spawnProbability <= 0.575) {
|
||||
berry = 'NormalBerry'
|
||||
} else if (spawnProbability <= 0.75) {
|
||||
berry = 'PoisonBerry'
|
||||
} else if (spawnProbability <= 0.85) {
|
||||
berry = 'SlowBerry'
|
||||
} else if (spawnProbability <= 0.95) {
|
||||
berry = 'UltraBerry'
|
||||
} else {
|
||||
berry = 'RandomBerry'
|
||||
}
|
||||
const time = Date.now()
|
||||
const posX = Math.random()
|
||||
const data: MultiplayerBerry = {
|
||||
Id: id,
|
||||
Type: berry,
|
||||
Timestamp: time,
|
||||
PosX: posX
|
||||
}
|
||||
multiplayerBerries.push(data)
|
||||
|
||||
for (const multiplayerClientInfo of multiplayerClients) {
|
||||
const client = Array.from(clients).find(
|
||||
client => client.id === multiplayerClientInfo.Id
|
||||
)
|
||||
if (!client) continue
|
||||
client.send({
|
||||
type: 'multiplayer_berry_spawn',
|
||||
id,
|
||||
berryType: berry,
|
||||
posX,
|
||||
timestamp: time
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
app.ws('/ws', {
|
||||
maxPayloadLength: 10 * 1024 * 1024,
|
||||
open (ws) {
|
||||
clients.add(ws)
|
||||
console.log(ws.id, 'connected')
|
||||
// console.log(ws.id, 'connected')
|
||||
},
|
||||
async message (ws, message: any) {
|
||||
// console.log('received:', message, 'from', ws.id)
|
||||
console.log('received:', message, 'from', ws.id)
|
||||
if (message == 'ping') ws.send('ping')
|
||||
if (message.type) {
|
||||
if (message.type == 'connected_with_current_scene') {
|
||||
if (
|
||||
message.type == 'multiplayer_player_move' &&
|
||||
message.x !== undefined &&
|
||||
message.y !== undefined &&
|
||||
message.f !== undefined
|
||||
) {
|
||||
const multiplayerInfo = multiplayerClients.find(
|
||||
client => client.Id == ws.id
|
||||
)
|
||||
if (!multiplayerInfo) return
|
||||
multiplayerInfo.PosX = Number(message.x)
|
||||
multiplayerInfo.PosY = Number(message.y)
|
||||
multiplayerInfo.Flipped = Boolean(message.f)
|
||||
const index = multiplayerClients.findIndex(client => client.Id == ws.id)
|
||||
if (index === -1) return
|
||||
multiplayerClients[index] = multiplayerInfo
|
||||
for (const multiplayerClientInfo of multiplayerClients) {
|
||||
if (ws.id == multiplayerClientInfo.Id) continue
|
||||
const client = Array.from(clients).find(
|
||||
client => client.id === multiplayerClientInfo.Id
|
||||
)
|
||||
if (!client) continue
|
||||
client.send({
|
||||
type: 'multiplayer_player_move',
|
||||
id: multiplayerInfo.UserID,
|
||||
x: Number(message.x),
|
||||
y: Number(message.y),
|
||||
f: Boolean(message.f)
|
||||
})
|
||||
}
|
||||
} else if (message.type == 'multiplayer_leave') {
|
||||
const multiplayerInfo = multiplayerClients.find(
|
||||
client => client.Id == ws.id
|
||||
)
|
||||
if (!multiplayerInfo) return
|
||||
multiplayerClients = multiplayerClients.filter(
|
||||
client => client.Id !== ws.id
|
||||
)
|
||||
for (const multiplayerClientInfo of multiplayerClients) {
|
||||
if (ws.id == multiplayerClientInfo.Id) continue
|
||||
const client = Array.from(clients).find(
|
||||
client => client.id === multiplayerClientInfo.Id
|
||||
)
|
||||
if (!client) continue
|
||||
client.send({
|
||||
type: 'multiplayer_player_leave',
|
||||
id: multiplayerInfo.UserID
|
||||
})
|
||||
}
|
||||
} else if (message.type == 'multiplayer_join_request' && message.auth) {
|
||||
const dbInfo0 = getDatabaseConnection(0)
|
||||
const dbInfo1 = getDatabaseConnection(1)
|
||||
|
||||
if (!dbInfo0 || !dbInfo1) {
|
||||
ws.send({
|
||||
type: 'multiplayer_join_response',
|
||||
accepted: false,
|
||||
denyReason: 'Unable to authenticate',
|
||||
key: null,
|
||||
players: []
|
||||
})
|
||||
return
|
||||
}
|
||||
const { connection: connection0, db: db0 } = dbInfo0
|
||||
const { connection: connection1, db: db1 } = dbInfo1
|
||||
|
||||
const ip = ws.remoteAddress
|
||||
const authResult = await checkAuthorization(
|
||||
message.auth as string,
|
||||
db0,
|
||||
ip
|
||||
)
|
||||
if (!authResult.valid) {
|
||||
connection0.end()
|
||||
connection1.end()
|
||||
ws.send({
|
||||
type: 'multiplayer_join_response',
|
||||
accepted: false,
|
||||
denyReason: 'Unable to authenticate',
|
||||
id: null,
|
||||
players: []
|
||||
})
|
||||
return
|
||||
}
|
||||
const userId = authResult.id
|
||||
|
||||
const result = await db0
|
||||
.select({
|
||||
username: users.username,
|
||||
admin: users.admin,
|
||||
token: users.token
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.execute()
|
||||
|
||||
if (result.length != 1) {
|
||||
connection0.end()
|
||||
connection1.end()
|
||||
ws.send({
|
||||
type: 'multiplayer_join_response',
|
||||
accepted: false,
|
||||
denyReason: 'Unable to authenticate',
|
||||
id: null,
|
||||
players: null
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const players: any = []
|
||||
for (const marketplaceInfo of multiplayerClients) {
|
||||
const userData = await db1
|
||||
.select({
|
||||
saveData: berryDashUserData.saveData
|
||||
})
|
||||
.from(berryDashUserData)
|
||||
.where(and(eq(berryDashUserData.id, marketplaceInfo.UserID)))
|
||||
.execute()
|
||||
|
||||
const savedata = userData[0].saveData
|
||||
? JSON.parse(userData[0].saveData)
|
||||
: null
|
||||
if (!savedata) {
|
||||
connection0.end()
|
||||
connection1.end()
|
||||
return
|
||||
}
|
||||
|
||||
const customIcon = savedata.bird?.customIcon?.selected ?? null
|
||||
const icon = customIcon ? 0 : savedata?.icon ?? 1
|
||||
const overlay = customIcon ? 1 : savedata?.overlay ?? 0
|
||||
const iconColor = customIcon
|
||||
? [255, 255, 255]
|
||||
: savedata?.settings?.colors?.icon ?? [255, 255, 255]
|
||||
const overlayColor = customIcon
|
||||
? [255, 255, 255]
|
||||
: savedata?.settings?.colors?.overlay ?? [255, 255, 255]
|
||||
|
||||
players.push({
|
||||
id: marketplaceInfo.UserID,
|
||||
username: result[0].username,
|
||||
admin: result[0].admin,
|
||||
timestamp: marketplaceInfo.JoinTimestamp,
|
||||
iconData: {
|
||||
icon,
|
||||
overlay,
|
||||
iconColor,
|
||||
overlayColor,
|
||||
customIcon: customIcon ?? ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const key = randomBytes(16).toString('hex')
|
||||
const time = Date.now()
|
||||
const data: MultiplayerClient = {
|
||||
Id: ws.id,
|
||||
Key: key,
|
||||
PosX: 0,
|
||||
PosY: -1,
|
||||
Flipped: false,
|
||||
Paused: false,
|
||||
FlaggedReason: null,
|
||||
Token: result[0].token, //Just to be safe
|
||||
Username: result[0].username,
|
||||
UserID: userId,
|
||||
Admin: result[0].admin,
|
||||
Platform: null,
|
||||
JoinTimestamp: time
|
||||
}
|
||||
multiplayerClients.push(data)
|
||||
ws.send({
|
||||
type: 'multiplayer_join_response',
|
||||
accepted: true,
|
||||
denyReason: null,
|
||||
players
|
||||
})
|
||||
|
||||
const userData = await db1
|
||||
.select({
|
||||
saveData: berryDashUserData.saveData
|
||||
})
|
||||
.from(berryDashUserData)
|
||||
.where(and(eq(berryDashUserData.id, userId)))
|
||||
.execute()
|
||||
|
||||
const savedata = userData[0].saveData
|
||||
? JSON.parse(userData[0].saveData)
|
||||
: null
|
||||
if (!savedata) {
|
||||
connection0.end()
|
||||
connection1.end()
|
||||
return
|
||||
}
|
||||
|
||||
const customIcon = savedata.bird?.customIcon?.selected ?? null
|
||||
const icon = customIcon ? 0 : savedata?.icon ?? 1
|
||||
const overlay = customIcon ? 1 : savedata?.overlay ?? 0
|
||||
const iconColor = customIcon
|
||||
? [255, 255, 255]
|
||||
: savedata?.settings?.colors?.icon ?? [255, 255, 255]
|
||||
const overlayColor = customIcon
|
||||
? [255, 255, 255]
|
||||
: savedata?.settings?.colors?.overlay ?? [255, 255, 255]
|
||||
|
||||
for (const multiplayerClientInfo of multiplayerClients) {
|
||||
if (ws.id == multiplayerClientInfo.Id) continue
|
||||
const client = Array.from(clients).find(
|
||||
client => client.id === multiplayerClientInfo.Id
|
||||
)
|
||||
if (!client) continue
|
||||
client.send({
|
||||
type: 'multiplayer_player_join',
|
||||
id: userId,
|
||||
username: result[0].username,
|
||||
admin: result[0].admin,
|
||||
timestamp: time,
|
||||
iconData: {
|
||||
icon,
|
||||
overlay,
|
||||
iconColor,
|
||||
overlayColor,
|
||||
customIcon: customIcon ?? ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
connection0.end()
|
||||
connection1.end()
|
||||
} else if (message.type == 'connected_with_current_scene') {
|
||||
if (
|
||||
!message.sceneName ||
|
||||
!message.Requester ||
|
||||
@@ -535,12 +835,33 @@ app.ws('/ws', {
|
||||
}
|
||||
}
|
||||
},
|
||||
close (ws) {
|
||||
async close (ws) {
|
||||
clients.forEach(client => {
|
||||
if (client.id === ws.id) clients.delete(client)
|
||||
})
|
||||
clientInfo = clientInfo.filter(client => client.Id !== ws.id)
|
||||
|
||||
console.log(ws.id, 'disconnected')
|
||||
const multiplayerInfo = multiplayerClients.find(
|
||||
client => client.Id == ws.id
|
||||
)
|
||||
if (multiplayerInfo) {
|
||||
multiplayerClients = multiplayerClients.filter(
|
||||
client => client.Id !== ws.id
|
||||
)
|
||||
for (const multiplayerClientInfo of multiplayerClients) {
|
||||
if (ws.id == multiplayerClientInfo.Id) continue
|
||||
const client = Array.from(clients).find(
|
||||
client => client.id === multiplayerClientInfo.Id
|
||||
)
|
||||
if (!client) continue
|
||||
client.send({
|
||||
type: 'multiplayer_player_leave',
|
||||
id: multiplayerInfo.UserID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(ws.id, 'disconnected')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1397,6 +1718,35 @@ app.get('/berrydash/render-icon', berryDashRenderIconGetHandler, {
|
||||
)
|
||||
})
|
||||
})
|
||||
app.get('/website/launcher/games', websiteLauncherGamesGetHandler, {
|
||||
detail: {
|
||||
hide: true
|
||||
},
|
||||
headers: t.Object({
|
||||
authorization: t.String(),
|
||||
'x-forwarded-for': t.Optional(
|
||||
t.String({
|
||||
hide: true
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
app.get('/website/launcher/game', websiteLauncherGameGetHandler, {
|
||||
detail: {
|
||||
hide: true
|
||||
},
|
||||
query: t.Object({
|
||||
id: t.String()
|
||||
}),
|
||||
headers: t.Object({
|
||||
authorization: t.String(),
|
||||
'x-forwarded-for': t.Optional(
|
||||
t.String({
|
||||
hide: true
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
app.all(
|
||||
'*',
|
||||
() =>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
import { games, launcherVersionManifest } from '../../../../lib/tables'
|
||||
import {
|
||||
getClientIp,
|
||||
getDatabaseConnection,
|
||||
jsonResponse
|
||||
} from '../../../../lib/util'
|
||||
import { Context } from 'elysia'
|
||||
import { checkAuthorization } from '../../../../lib/auth'
|
||||
|
||||
export const handler = async (context: Context) => {
|
||||
const dbResult = getDatabaseConnection(0)
|
||||
if (!dbResult) return
|
||||
const { connection, db } = dbResult
|
||||
|
||||
const gameId = Number(context.query.id)
|
||||
const ip = getClientIp(context)
|
||||
const authorizationToken = context.headers.authorization
|
||||
const authResult = await checkAuthorization(
|
||||
authorizationToken as string,
|
||||
db,
|
||||
ip
|
||||
)
|
||||
if (!authResult.valid) {
|
||||
connection.end()
|
||||
return
|
||||
}
|
||||
if (![1, 2].includes(authResult.id)) {
|
||||
connection.end()
|
||||
return
|
||||
}
|
||||
|
||||
const gameInfo = await db
|
||||
.select({ name: games.name, categoryNames: games.categoryNames })
|
||||
.from(games)
|
||||
.where(eq(games.id, gameId))
|
||||
.execute()
|
||||
if (gameInfo.length != 1) {
|
||||
connection.end()
|
||||
return
|
||||
}
|
||||
|
||||
const versions = await db
|
||||
.select()
|
||||
.from(launcherVersionManifest)
|
||||
.where(eq(launcherVersionManifest.game, gameId))
|
||||
.orderBy(desc(launcherVersionManifest.place))
|
||||
.execute()
|
||||
|
||||
connection.end()
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
name: gameInfo[0].name,
|
||||
categories: JSON.parse(gameInfo[0].categoryNames),
|
||||
versions
|
||||
},
|
||||
200,
|
||||
false
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { checkAuthorization } from '../../../../lib/auth'
|
||||
import { games } from '../../../../lib/tables'
|
||||
import {
|
||||
getClientIp,
|
||||
getDatabaseConnection,
|
||||
jsonResponse
|
||||
} from '../../../../lib/util'
|
||||
import { Context } from 'elysia'
|
||||
|
||||
export const handler = async (context: Context) => {
|
||||
const dbResult = getDatabaseConnection(0)
|
||||
if (!dbResult) return jsonResponse([], 500, false)
|
||||
const { connection, db } = dbResult
|
||||
|
||||
const ip = getClientIp(context)
|
||||
const authorizationToken = context.headers.authorization
|
||||
const authResult = await checkAuthorization(
|
||||
authorizationToken as string,
|
||||
db,
|
||||
ip
|
||||
)
|
||||
if (!authResult.valid) {
|
||||
connection.end()
|
||||
return jsonResponse(
|
||||
{ success: false, message: 'Unauthorized', data: null },
|
||||
401
|
||||
)
|
||||
}
|
||||
if (![1, 2].includes(authResult.id)) {
|
||||
connection.end()
|
||||
return jsonResponse(
|
||||
{ success: false, message: 'Unauthorized', data: null },
|
||||
401
|
||||
)
|
||||
}
|
||||
|
||||
const gamesList = await db
|
||||
.select({ id: games.id, name: games.name })
|
||||
.from(games)
|
||||
.execute()
|
||||
|
||||
connection.end()
|
||||
|
||||
return jsonResponse(gamesList, 200, false)
|
||||
}
|
||||
Reference in new issue
Block a user