Implement gacha history record subsystem

* Frontend is not very beautiful yet
* Didn't include too much `some anime game` data in the page to avoid being DMCA'd
This commit is contained in:
mingjun97 2022-05-01 12:49:44 -07:00 committed by Melledy
parent 22a651b4aa
commit 8cf4ef59ec
10 changed files with 393 additions and 6 deletions

176
data/gacha_records.html Normal file
View File

@ -0,0 +1,176 @@
<html>
<head>
<script>
// Debug entry
// record = [
// {"time": 10000341, "item": 1001},
// {"time": 10000342, "item": 1002},
// {"time": 10000343, "item": 1003},
// ];
// maxPage = 5;
// in production environment
record = {{REPLACE_RECORD}};
maxPage = {{REPLACE_MAXPAGE}};
// TODO: implement this mapper by yourself
// I don't want to put real items' name here to avoid being DMCA'd
mappings = {
'en-us': {
200: "Standard",
301: "Event Avatar",
302: "Event Weapon",
1041 : ["M0n4", "yellow"],
1032 : ["B4nn477", "purple"],
1035 : ["77", "yellow"]
},
'zh-cn': {
// encoding issues here, maybe we should consider load mappings remotely
// will display as "锟斤铐锟斤铐锟斤铐", lmao
// 200: "常驻",
// 301: "角色UP-1",
// 302: "武器UP"
200: "Standard",
301: "Event Avatar",
302: "Event Weapon",
}
};
mappings['default'] = mappings['en-us'];
</script>
<!-- TODO: Refine the CSS -->
<style>
a {
text-decoration: none !important;
}
.content {
width: 400px;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
}
.content .navbar {
margin: auto;
width: fit-content;
padding-top: 5px;
padding-bottom: 30px;
}
.yellow {
color: yellow;
}
.blue {
color: rgb(75, 107, 251);
}
.purple {
color: rgb(242, 40, 242);
}
</style>
</head>
<body style="background: skyblue;">
<div class="content">
<h1>Gacha Records</h1>
<h2 id="gacha-type"></h2>
<br/>
<div style="width: fit-content">
<table border="1">
<tbody id="container">
<tr>
<th>Time</th>
<th>Item</th>
</tr>
</tbody>
</table>
</div>
<div class="navbar">
<a href="" id="prev"> &lt; </a>
<span id="curpage">1</span>
<a href="" id="next"> &gt; </a>
</div>
</div>
<script>
var lang = new window.URLSearchParams(window.location.search).get("lang");
function itemMapper(itemID) {
if (mappings[lang] != null && mappings[lang][itemID] != null) {
var entry = mappings[lang][itemID];
if (entry){
return "<span class='" + entry[1] + "'>" + entry[0] + "</span>";
}
} else {
if (mappings['default'][itemID] != null) {
var entry = mappings['default'][itemID];
if (entry){
return "<span class='" + entry[1] + "'>" + entry[0] + "</span>";
}
}
}
return "<span class='blue'>" + itemID + "</span>";
}
function dateFormatter(timeStamp) {
var date = new Date(timeStamp);
if (lang == "en-us" || lang == null) { // MM/DD/YYYY hh:mm:ss.SSS
return String(date.getMonth()+1).padStart(2, "0") +
"/"+String(date.getDate()).padStart(2, "0")+
"/"+date.getFullYear()+
" "+String(date.getHours()).padStart(2, "0")+
":"+String(date.getMinutes()).padStart(2, "0")+
":"+String(date.getSeconds()).padStart(2, "0")+
"."+date.getMilliseconds();
} else if (lang == "zh-cn") { // YYYY/MM/DD hh:mm:ss.SSS
return date.getFullYear()+
"/" + String(date.getMonth()+1).padStart(2, "0") +
"/"+String(date.getDate()).padStart(2, "0")+
" "+String(date.getHours()).padStart(2, "0")+
":"+String(date.getMinutes()).padStart(2, "0")+
":"+String(date.getSeconds()).padStart(2, "0")+
"."+date.getMilliseconds();
}
}
(function (){
var container = document.getElementById("container");
record.forEach(element => {
var e = document.createElement("tr");
e.innerHTML= "<td>" + dateFormatter(element.time) + "</td><td>" + itemMapper(element.item) + "</td>";
container.appendChild(e);
});
// setup pagenation buttons
var page = parseInt(new window.URLSearchParams(window.location.search).get("p"));
if (!page){
page = 0;
}
document.getElementById("curpage").innerText = page + 1;
var href = new URL(window.location);
href.searchParams.set("p", page - 1);
document.getElementById("prev").href = href.toString();
href.searchParams.set("p", page + 1);
document.getElementById("next").href = href.toString();
if (page <= 0) {
document.getElementById("prev").style.display = "none";
}
if (page >= maxPage - 1) {
document.getElementById("next").style.display = "none";
}
// setup gacha type info
var gachaType = new window.URLSearchParams(window.location.search).get("gachaType");
var gachaString;
if (mappings[lang] != null && mappings[lang][gachaType] != null) {
gachaString = mappings[lang][gachaType];
}else{
gachaString = mappings['default'][gachaType];
if (gachaString == null) {
gachaString = gachaType;
}
}
document.getElementById("gacha-type").innerText = gachaString;
})();
</script>
</body>
</html>

View File

@ -3,11 +3,14 @@ package emu.grasscutter.database;
import java.util.List;
import com.mongodb.client.result.DeleteResult;
import dev.morphia.query.FindOptions;
import dev.morphia.query.Sort;
import dev.morphia.query.experimental.filters.Filters;
import emu.grasscutter.GameConstants;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.friends.Friendship;
import emu.grasscutter.game.gacha.GachaRecord;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
@ -78,6 +81,11 @@ public final class DatabaseHelper {
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("token", token)).first();
}
public static Account getAccountBySessionKey(String sessionKey) {
if(sessionKey == null) return null;
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("sessionKey", sessionKey)).first();
}
public static Account getAccountById(String uid) {
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("_id", uid)).first();
}
@ -181,5 +189,36 @@ public final class DatabaseHelper {
)).first();
}
public static List<GachaRecord> getGachaRecords(int ownerId, int page, int gachaType){
return getGachaRecords(ownerId, page, gachaType, 10);
}
public static List<GachaRecord> getGachaRecords(int ownerId, int page, int gachaType, int pageSize){
return DatabaseManager.getDatastore().find(GachaRecord.class).filter(
Filters.eq("ownerId", ownerId),
Filters.eq("gachaType", gachaType)
).iterator(new FindOptions()
.sort(Sort.descending("transactionDate"))
.skip(pageSize * page)
.limit(pageSize)
).toList();
}
public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType){
return getGachaRecordsMaxPage(ownerId, page, gachaType, 10);
}
public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType, int pageSize){
long count = DatabaseManager.getDatastore().find(GachaRecord.class).filter(
Filters.eq("ownerId", ownerId),
Filters.eq("gachaType", gachaType)
).count();
return count / 10 + (count % 10 > 0 ? 1 : 0 );
}
public static void saveGachaRecord(GachaRecord gachaRecord){
DatabaseManager.getDatastore().save(gachaRecord);
}
public static char AWJVN = 'e';
}

View File

@ -16,6 +16,7 @@ import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.friends.Friendship;
import emu.grasscutter.game.gacha.GachaRecord;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
@ -28,7 +29,7 @@ public final class DatabaseManager {
private static Datastore dispatchDatastore;
private static final Class<?>[] mappedClasses = new Class<?>[] {
DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class
DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, GachaRecord.class
};
public static Datastore getDatastore() {

View File

@ -91,9 +91,21 @@ public class GachaBanner {
return eventChance;
}
@Deprecated
public GachaInfo toProto() {
String record = "http://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + "/gacha";
return toProto("");
}
public GachaInfo toProto(String sessionKey) {
String record = "https://"
+ (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ?
Grasscutter.getConfig().getDispatchOptions().Ip :
Grasscutter.getConfig().getDispatchOptions().PublicIp)
+ ":"
+ Integer.toString(Grasscutter.getConfig().getDispatchOptions().PublicPort == 0 ?
Grasscutter.getConfig().getDispatchOptions().Port :
Grasscutter.getConfig().getDispatchOptions().PublicPort)
+ "/gacha?s=" + sessionKey + "&gachaType=" + gachaType;
// Grasscutter.getLogger().info("record = " + record);
GachaInfo.Builder info = GachaInfo.newBuilder()
.setGachaType(this.getGachaType())
.setScheduleId(this.getScheduleId())

View File

@ -14,6 +14,7 @@ import com.sun.nio.file.SensitivityWatchEventModifier;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.def.ItemData;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.gacha.GachaBanner.BannerType;
import emu.grasscutter.game.inventory.GameItem;
@ -197,6 +198,10 @@ public class GachaManager {
continue;
}
// Write gacha record
GachaRecord gachaRecord = new GachaRecord(itemId, player.getUid(), gachaType);
DatabaseHelper.saveGachaRecord(gachaRecord);
// Create gacha item
GachaItem.Builder gachaItem = GachaItem.newBuilder();
int addStardust = 0, addStarglitter = 0;
@ -321,6 +326,7 @@ public class GachaManager {
}
}
@Deprecated
private synchronized GetGachaInfoRsp createProto() {
GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345);
@ -331,11 +337,25 @@ public class GachaManager {
return proto.build();
}
private synchronized GetGachaInfoRsp createProto(String sessionKey) {
GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345);
for (GachaBanner banner : getGachaBanners().values()) {
proto.addGachaInfoList(banner.toProto(sessionKey));
}
return proto.build();
}
@Deprecated
public GetGachaInfoRsp toProto() {
if (this.cachedProto == null) {
this.cachedProto = createProto();
}
return this.cachedProto;
}
public GetGachaInfoRsp toProto(String sessionKey) {
return createProto(sessionKey);
}
}

View File

@ -0,0 +1,75 @@
package emu.grasscutter.game.gacha;
import java.util.Date;
import org.bson.types.ObjectId;
import dev.morphia.annotations.*;
@Entity(value = "gachas", useDiscriminator = false)
public class GachaRecord {
@Id private ObjectId id;
@Indexed private int ownerId;
private Date transactionDate;
private int itemID;
@Indexed private int gachaType;
public GachaRecord() {}
public GachaRecord(int itemId ,int ownerId, int gachaType){
this.transactionDate = new Date();
this.itemID = itemId;
this.ownerId = ownerId;
this.gachaType = gachaType;
}
public int getOwnerId() {
return ownerId;
}
public void setOwnerId(int ownerId) {
this.ownerId = ownerId;
}
public int getGachaType() {
return gachaType;
}
public void setGachaType(int type) {
this.gachaType = type;
}
public Date getTransactionDate() {
return transactionDate;
}
public void setTransactionDate(Date transactionDate) {
this.transactionDate = transactionDate;
}
public int getItemID() {
return itemID;
}
public void setItemID(int itemID) {
this.itemID = itemID;
}
public ObjectId getId(){
return id;
}
public void setId(ObjectId id) {
this.id = id;
}
public String toString() {
return toJsonString();
}
public String toJsonString() {
return "{\"time\": " + this.transactionDate.getTime() + ",\"item\":" + this.itemID + "}";
}
}

View File

@ -18,6 +18,7 @@ import emu.grasscutter.server.dispatch.json.*;
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData;
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
import emu.grasscutter.server.http.gacha.GachaRecordHandler;
import emu.grasscutter.utils.FileUtils;
import express.Express;
import org.eclipse.jetty.server.Connector;
@ -485,7 +486,7 @@ public final class DispatchServer {
// webstatic-sea.hoyoverse.com
httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}"));
httpServer.get("/gacha", (req, res) -> res.send("<!doctype html><html lang=\"en\"><head><title>Gacha</title></head><body></body></html>"));
httpServer.get("/gacha", new GachaRecordHandler());
httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
Grasscutter.getLogger().info("[Dispatch] Dispatch server started on port " + httpServer.raw().port());

View File

@ -0,0 +1,52 @@
package emu.grasscutter.server.http.gacha;
import java.io.File;
import java.io.IOException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.utils.FileUtils;
import express.http.HttpContextHandler;
import express.http.Request;
import express.http.Response;
public final class GachaRecordHandler implements HttpContextHandler {
String render_template;
public GachaRecordHandler() {
File template = new File(Grasscutter.getConfig().DATA_FOLDER + "gacha_records.html");
if (template.exists()) {
// Load from cache
render_template = new String(FileUtils.read(template));
} else {
render_template = "{{REPLACE_RECORD}}";
}
}
@Override
public void handle(Request req, Response res) throws IOException {
// Grasscutter.getLogger().info( req.query().toString() );
String sessionKey = req.query("s");
int page = 0;
int gachaType = 0;
if (req.query("p") != null) {
page = Integer.valueOf(req.query("p"));
}
if (req.query("gachaType") != null) {
gachaType = Integer.valueOf(req.query("gachaType"));
}
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if (account != null) {
String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), page, gachaType).toString();
// Grasscutter.getLogger().info(records);
String response = render_template.replace("{{REPLACE_RECORD}}", records)
.replace("{{REPLACE_MAXPAGE}}", String.valueOf(DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType)));
res.send(response);
} else {
res.send("404");
}
}
}

View File

@ -11,7 +11,10 @@ public class HandlerGetGachaInfoReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
session.send(new PacketGetGachaInfoRsp(session.getServer().getGachaManager()));
session.send(new PacketGetGachaInfoRsp(session.getServer().getGachaManager(),
// TODO: use other Nonce/key insteadof session key to ensure the overall security for the player
session.getPlayer().getAccount().getSessionKey())
);
}
}

View File

@ -6,9 +6,17 @@ import emu.grasscutter.net.packet.PacketOpcodes;
public class PacketGetGachaInfoRsp extends BasePacket {
@Deprecated
public PacketGetGachaInfoRsp(GachaManager manager) {
super(PacketOpcodes.GetGachaInfoRsp);
this.setData(manager.toProto());
}
public PacketGetGachaInfoRsp(GachaManager manager, String sessionKey) {
super(PacketOpcodes.GetGachaInfoRsp);
this.setData(manager.toProto(sessionKey));
}
}