mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-30 15:08:43 +00:00
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:
parent
22a651b4aa
commit
8cf4ef59ec
176
data/gacha_records.html
Normal file
176
data/gacha_records.html
Normal 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"> < </a>
|
||||
<span id="curpage">1</span>
|
||||
<a href="" id="next"> > </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>
|
@ -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';
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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())
|
||||
|
@ -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;
|
||||
@ -196,6 +197,10 @@ public class GachaManager {
|
||||
if (itemData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write gacha record
|
||||
GachaRecord gachaRecord = new GachaRecord(itemId, player.getUid(), gachaType);
|
||||
DatabaseHelper.saveGachaRecord(gachaRecord);
|
||||
|
||||
// Create gacha item
|
||||
GachaItem.Builder gachaItem = GachaItem.newBuilder();
|
||||
@ -321,6 +326,7 @@ public class GachaManager {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private synchronized GetGachaInfoRsp createProto() {
|
||||
GetGachaInfoRsp.Builder proto = GetGachaInfoRsp.newBuilder().setGachaRandom(12345);
|
||||
|
||||
@ -330,12 +336,26 @@ 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);
|
||||
}
|
||||
}
|
||||
|
75
src/main/java/emu/grasscutter/game/gacha/GachaRecord.java
Normal file
75
src/main/java/emu/grasscutter/game/gacha/GachaRecord.java
Normal 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 + "}";
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user