Early version of multiplayer

This commit is contained in:
Lncvrt committed 2026-03-04 22:32:14 -07:00
1 parent 9600a8ea6b
commit 8001c630c2
4 files changed
+491 -9

No files matched your search

+29 -3
View File
@@ -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
View File
@@ -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(
'*',
() =>
+61
View File
@@ -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
)
}
+45
View File
@@ -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)
}