Skip to content

Commit

Permalink
feat: set up Firebase
Browse files Browse the repository at this point in the history
  • Loading branch information
R-unic committed Mar 14, 2024
1 parent ff0d112 commit 52eb6c6
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 89 deletions.
6 changes: 0 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"@flamework/core": "^1.1.1",
"@flamework/networking": "^1.1.1",
"@rbxts/builders": "^1.0.2",
"@rbxts/datastore2": "^1.4.0-ts.0",
"@rbxts/gamejoy": "^1.1.4",
"@rbxts/janitor": "^1.15.7-ts.0",
"@rbxts/object-utils": "^1.0.4",
Expand Down
11 changes: 11 additions & 0 deletions src/client/controllers/initialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Controller, type OnStart } from "@flamework/core";
import { Events } from "client/network";
import Log from "shared/logger";

@Controller()
export class InitializationController implements OnStart {
public onStart(): void {
Events.data.updated.connect((directory, value) => Log.info(`DATA UPDATED! ${directory}: ${value}`));
Events.data.initialize();
}
}
60 changes: 60 additions & 0 deletions src/server/classes/firebase.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
interface Headers {
[name: string]: string;
}

interface FirebaseService {
/**
* Sets whether Firebase's data can be updated from server. Data can still be read from realtime Database regardless.
* @param use bool Using Firebase
*/
useFirebase(use: boolean): void;
/**
* Sets whether Firebase's data can be updated from server. Data can still be read from realtime Database regardless.
* @param name Given name of a JSON Object in the Realtime Database.
* @param scope Optional scope.
* @returns Firebase database
*/
fetch(dataKey: string, database?: string): Firebase;
}

interface Firebase {
/**
* A method to get a datastore with the same name and scope.
* @returns Roblox GlobalDataStore representing the Firebase database.
*/
getDatastore(): GlobalDataStore;
/**
* Returns the value of the entry in the database JSON Object with the given key.
* @param directory Directory of the value that you are look for. E.g. "PlayerSaves" or "PlayerSaves/Stats".
* @returns Value associated with directory.
*/
get<T>(directory: string): T;
/**
* Sets the value of the key. This overwrites any existing data stored in the key.
* @param directory Directory of the value that you are look for. E.g. "PlayerSaves" or "PlayerSaves/Stats".
* @param value Value can be any basic data types. It's recommened you HttpService:JSONEncode() your values before passing it through.
* @param headers Optional HTTPRequest Header overwrite. Default is {["X-HTTP-Method-Override"]="PUT"}.
*/
set<T>(directory: string, value: T, headers?: Headers): void;
/**'
* Increments the value of a particular key and returns the incremented value.
* @param directory Directory of the value that you are look for. E.g. "PlayerSaves" or "PlayerSaves/Stats".
* @param delta The amount to increment by.
* @returns The new incremented value
*/
increment(directory: string, delta: number): number;
/**
* Removes the given key from the data store and returns the value associated with that key.
* @param directory Directory of the value that you are look for. E.g. "PlayerSaves" or "PlayerSaves/Stats".
*/
delete(directory: string): void;
/**
* Retrieves the value of a key from a data store and updates it with a new value.
* @param directory Directory of the value that you are look for. E.g. "PlayerSaves" or "PlayerSaves/Stats".
* @param callback Works similarly to Roblox's GlobalDatastore:UpdateAsync().
*/
update<T>(directory: string, callback: (currentData: T) => T): void;
}

declare const Firebase: FirebaseService;
export = Firebase;
93 changes: 93 additions & 0 deletions src/server/classes/firebase.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
--TODO: store in datastore
--== Configuration;
local defaultDatabase = "https://roblox-tabletop-lounge-default-rtdb.firebaseio.com/"; -- Set your database link
local authenticationToken = "9vql32DWOoZGjQzOiO28XWGhsexcPN8ejId8FsTF"; -- Authentication Token

--== Variables;
local HttpService = game:GetService("HttpService");
local DataStoreService = game:GetService("DataStoreService");

local FirebaseService = {};
local UseFirebase = true;

--== Script;

function FirebaseService:useFirebase(use)
UseFirebase = use;
end

function FirebaseService:fetch(name, database)
database = database or defaultDatabase;
local datastore = DataStoreService:GetDataStore(name);
local databaseName = database..HttpService:UrlEncode(name);
local authentication = ".json?auth="..authenticationToken;
local Firebase = {};

function Firebase:getDatastore()
return datastore;
end

function Firebase:get(directory)
--== Firebase Get;
local data;
local endpoint = databaseName..HttpService:UrlEncode(directory and "/"..directory or "")..authentication;
local tries = 0; repeat until pcall(function()
tries += 1;
data = HttpService:GetAsync(endpoint, true);
end) or tries > 2;
return if data ~= nil then HttpService:JSONDecode(data) else nil;
end

function Firebase:set(directory, value, headers)
if not UseFirebase then return end
if value == "[]" then self:delete(directory); return end;

--== Firebase Set;
headers = headers or {["X-HTTP-Method-Override"]="PUT"};
local replyJson = "";
local endpoint = databaseName..HttpService:UrlEncode(directory and "/"..directory or "")..authentication;
local success, errorMessage = pcall(function()
replyJson = HttpService:PostAsync(
endpoint, HttpService:JSONEncode(value),
Enum.HttpContentType.ApplicationUrlEncoded, false, headers
);
end);
if not success then
warn("[Firebase] Error: "..errorMessage);
pcall(function()
replyJson = HttpService:JSONDecode(replyJson or "[]");
end)
end
end

function Firebase:delete(directory)
if not UseFirebase then return end
self:set(directory, nil, {["X-HTTP-Method-Override"]="DELETE"});
end

function Firebase:increment(directory, delta)
delta = delta or 1;
if type(delta) ~= "number" then warn("[Firebase] Error: Increment delta is not a number for key ("..directory.."), delta(",delta,")"); return end;
local data = self:get(directory) or 0;
if data and type(data) == "number" then
data += delta;
self:set(directory, data);
else
warn("[Firebase] Error: Invalid data type to increment for key ("..directory..")");
end
return data;
end

function Firebase:update(directory, callback)
local data = self:get(directory);
local callbackData = callback(data);
if callbackData then
self:set(directory, callbackData);
end
end

print("fetched firebase")
return Firebase;
end

return FirebaseService;
1 change: 1 addition & 0 deletions src/server/classes/games/uno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const CARD_PILE_DISTANCE = 1; // distance between the pile of cards to draw and
const STACKING_ALLOWED = false; // standard uno

// TODO: track score
// TODO: winners
export default class Uno extends CardGame<Game.Uno> {
public static readonly name = Game.Uno;

Expand Down
72 changes: 0 additions & 72 deletions src/server/services/data.ts

This file was deleted.

60 changes: 60 additions & 0 deletions src/server/services/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { OnInit, Service } from "@flamework/core";
import Signal from "@rbxts/signal";

import { DataValue } from "shared/data-models/generic";
import { Events, Functions } from "server/network";
import Firebase from "server/classes/firebase";
import Log from "shared/logger";

import type { LogStart } from "shared/hooks";

const PlayerData = Firebase.fetch("playerData");

@Service()
export class DatabaseService implements OnInit, LogStart {
public readonly loaded = new Signal<(player: Player) => void>;

public onInit(): void {
Events.data.initialize.connect((player) => this.setup(player));
Events.data.set.connect((player, key, value) => this.set(player, key, value));
Events.data.increment.connect((player, key, amount) => this.increment(player, key, amount))
Functions.data.get.setCallback((player, key) => this.get(player, key));
}

public get<T extends DataValue>(player: Player, directory: string): T {
const fullDirectory = this.getDirectoryForPlayer(player, directory);
return PlayerData.get(fullDirectory);
}

public set<T extends DataValue>(player: Player, directory: string, value: T): void {
const fullDirectory = this.getDirectoryForPlayer(player, directory);
PlayerData.set(fullDirectory, value);
Events.data.updated(player, fullDirectory, value);
}

public increment(player: Player, directory: string, amount = 1): number {
const fullDirectory = this.getDirectoryForPlayer(player, directory);
return PlayerData.increment(fullDirectory, amount);
}

public delete(player: Player, directory: string): void {
const fullDirectory = this.getDirectoryForPlayer(player, directory);
return PlayerData.delete(fullDirectory);
}

private setup(player: Player): void {
this.initialize(player, "playtime", 0);
this.loaded.Fire(player);
Log.info("Initialized data");
}

private initialize<T extends DataValue>(player: Player, directory: string, initialValue: T): void {
const fullDirectory = this.getDirectoryForPlayer(player, directory);
const value = PlayerData.get<Maybe<T>>(fullDirectory) ?? initialValue;
this.set(player, directory, value);
}

private getDirectoryForPlayer(player: Player, directory: string) {
return `${player.UserId}/${directory}`;
}
}
9 changes: 2 additions & 7 deletions src/shared/data-models/generic.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
export interface GameDataModel {
// put all of your data fields and types here
// for example:
gold: number;
gems: number;
playtime: number;
}

export type DataValue = GameDataModel[DataKey];
export type DataKey = keyof GameDataModel;

export const DataKeys: DataKey[] = [
// put all of the keys for your data here
// using the last example's data, you would write:
"gold", "gems"
"playtime"
];
3 changes: 1 addition & 2 deletions src/shared/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type Game from "./structs/game";
interface ServerEvents {
data: {
initialize(): void;
loaded(): void;
set(key: DataKey, value: DataValue): void;
increment(key: ExtractKeys<GameDataModel, number>, amount?: number): void;
};
Expand All @@ -22,7 +21,7 @@ interface ServerEvents {

interface ClientEvents {
data: {
update(key: DataKey, value: DataValue): void;
updated(directory: string, value: DataValue): void;
};
games: {
toggleCamera(tableID: string, on: boolean): void;
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/shared/utilities/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReplicatedFirst, RunService as Runtime } from "@rbxts/services";
import StringUtils from "@rbxts/string-utils";

import { StorableVector3 } from "../data-models/utility";
import { StorableVector3 } from "../structs/common";
import { Exception } from "../exceptions";

const { floor, log, abs, max, min } = math;
Expand Down

0 comments on commit 52eb6c6

Please sign in to comment.