Merge branch 'development' into dev-quests

This commit is contained in:
Melledy 2022-05-11 12:39:28 -07:00
commit 364dc3bab9
19 changed files with 467 additions and 130 deletions

1
.gitignore vendored
View File

@ -69,6 +69,7 @@ language/
languages/ languages/
gacha-mapping.js gacha-mapping.js
data/gacha_mappings.js data/gacha_mappings.js
BuildConfig.java
# macOS # macOS
.DS_Store .DS_Store

View File

@ -45,6 +45,7 @@ targetCompatibility = JavaVersion.VERSION_17
group = 'xyz.grasscutters' group = 'xyz.grasscutters'
version = '1.1.1-dev' version = '1.1.1-dev'
sourceCompatibility = 17 sourceCompatibility = 17
targetCompatibility = 17 targetCompatibility = 17
@ -97,6 +98,7 @@ application {
mainClassName = 'emu.grasscutter.Grasscutter' mainClassName = 'emu.grasscutter.Grasscutter'
} }
jar { jar {
manifest { manifest {
attributes 'Main-Class': 'emu.grasscutter.Grasscutter' attributes 'Main-Class': 'emu.grasscutter.Grasscutter'
@ -226,6 +228,23 @@ javadoc {
} }
} }
task injectGitHash {
def gitCommitHash = {
try {
return 'git rev-parse --verify --short HEAD'.execute().text.trim()
} catch (e) {
return "GIT_NOT_FOUND"
}
}
new File(projectDir, "src/main/java/emu/grasscutter/BuildConfig.java").text = """
package emu.grasscutter;
public class BuildConfig {
public static final String VERSION = \"${version}\";
public static final String GIT_HASH = \"${gitCommitHash()}\";
}
"""
}
processResources { processResources {
dependsOn "generateProto" dependsOn "generateProto"
} }

121
data/gacha_details.html Normal file
View File

@ -0,0 +1,121 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
<style>
body {
background-color: #f0f0f0;
}
p {
font-weight:300;
}
a,a:hover {
text-decoration:none !important;
color:#626976;
}
.content {
padding:3rem 0;
}
.container {
color:#626976;
position: relative;
}
h2 {
font-size:20px;
}
h3 {
font-size:16px;
}
</style>
<title>Banner Details</title>
<script type="text/javascript" src="/gacha/mappings"></script>
</head>
<body>
<div class="content">
<div class="container">
<h2 class="mb-5">{{TITLE}}</h2>
<h3 class="">{{AVAILABLE_FIVE_STARS}}</h3>
<hr />
<ul id="5-star-list">
</ul>
<h3 class="">{{AVAILABLE_FOUR_STARS}}</h3>
<hr />
<ul id="4-star-list">
</ul>
<h3 class="">{{AVAILABLE_THREE_STARS}}</h3>
<hr />
<ul id="3-star-list">
</ul>
</div>
</div>
<footer>
<div class="copyright">
<div class="container">
<div class="row">
<div class="col-md-6">
<span>
Template by BecodReyes. All rights reserved.
</span>
</div>
<div class="col-md-6">
<ul style="float:right">
<li class="list-inline-item">
<a href="https://github.com/Grasscutters/Grasscutter">Github</a>
</li>
<li class="list-inline-item">·</li>
<li class="list-inline-item">
<a href="https://github.com/Grasscutters/Grasscutter/blob/stable/LICENSE">License</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</footer>
<script>
var fiveStarItems = {{FIVE_STARS}};
var fourStarItems = {{FOUR_STARS}};
var threeStarItems = {{THREE_STARS}};
var lang = "{{LANGUAGE}}";
function getNameForId(itemId) {
if (mappings[lang] != null && mappings[lang][itemId] != null) {
return mappings[lang][itemId][0];
}
else if (mappings["en-us"] != null && mappings["en-us"][itemId] != null) {
return mappings["en-us"][itemId][0];
}
return itemId.toString();
}
fiveStarList = document.getElementById("5-star-list");
fourStarList = document.getElementById("4-star-list");
threeStarList = document.getElementById("3-star-list");
fiveStarItems.forEach(element => {
var entry = document.createElement("li");
entry.innerHTML = getNameForId(element);
fiveStarList.appendChild(entry);
});
fourStarItems.forEach(element => {
var entry = document.createElement("li");
entry.innerHTML = getNameForId(element);
fourStarList.appendChild(entry);
});
threeStarItems.forEach(element => {
var entry = document.createElement("li");
entry.innerHTML = getNameForId(element);
threeStarList.appendChild(entry);
});
</script>
</body>
</html>

View File

@ -29,6 +29,7 @@ import emu.grasscutter.server.dispatch.DispatchServer;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.tools.Tools; import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Crypto;
import emu.grasscutter.BuildConfig;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -82,6 +83,9 @@ public final class Grasscutter {
case "-gachamap" -> { case "-gachamap" -> {
Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true; Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true;
} }
case "-version" -> {
System.out.println("Grasscutter version: " + BuildConfig.VERSION + "-" + BuildConfig.GIT_HASH); exitEarly = true;
}
} }
} }
@ -186,6 +190,7 @@ public final class Grasscutter {
} }
getLogger().info(translate("messages.status.done")); getLogger().info(translate("messages.status.done"));
getLogger().info(translate("messages.status.version", BuildConfig.VERSION, BuildConfig.GIT_HASH));
String input = null; String input = null;
boolean isLastInterrupted = false; boolean isLastInterrupted = false;
while (true) { while (true) {

View File

@ -78,25 +78,25 @@ public final class DatabaseHelper {
} }
public static Account getAccountByName(String username) { public static Account getAccountByName(String username) {
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("username", username)).first(); return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("username", username)).first();
} }
public static Account getAccountByToken(String token) { public static Account getAccountByToken(String token) {
if(token == null) return null; if(token == null) return null;
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("token", token)).first(); return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("token", token)).first();
} }
public static Account getAccountBySessionKey(String sessionKey) { public static Account getAccountBySessionKey(String sessionKey) {
if(sessionKey == null) return null; if(sessionKey == null) return null;
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("sessionKey", sessionKey)).first(); return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("sessionKey", sessionKey)).first();
} }
public static Account getAccountById(String uid) { public static Account getAccountById(String uid) {
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("_id", uid)).first(); return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("_id", uid)).first();
} }
public static Account getAccountByPlayerId(int playerId) { public static Account getAccountByPlayerId(int playerId) {
return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("playerId", playerId)).first(); return DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("playerId", playerId)).first();
} }
public static void deleteAccount(Account target) { public static void deleteAccount(Account target) {
@ -105,39 +105,39 @@ public final class DatabaseHelper {
// database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ... // database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ...
// Delete Mail.class data // Delete Mail.class data
DatabaseManager.getDatabase().getCollection("mail").deleteMany(eq("ownerUid", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("mail").deleteMany(eq("ownerUid", target.getPlayerUid()));
// Delete Avatar.class data // Delete Avatar.class data
DatabaseManager.getDatabase().getCollection("avatars").deleteMany(eq("ownerId", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("avatars").deleteMany(eq("ownerId", target.getPlayerUid()));
// Delete GachaRecord.class data // Delete GachaRecord.class data
DatabaseManager.getDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid()));
// Delete GameItem.class data // Delete GameItem.class data
DatabaseManager.getDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid()));
// Delete GameMainQuest.class data // Delete GameMainQuest.class data
DatabaseManager.getDatabase().getCollection("quests").deleteMany(eq("ownerUid", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("quests").deleteMany(eq("ownerUid", target.getPlayerUid()));
// Delete friendships. // Delete friendships.
// Here, we need to make sure to not only delete the deleted account's friendships, // Here, we need to make sure to not only delete the deleted account's friendships,
// but also all friendship entries for that account's friends. // but also all friendship entries for that account's friends.
DatabaseManager.getDatabase().getCollection("friendships").deleteMany(eq("ownerId", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("ownerId", target.getPlayerUid()));
DatabaseManager.getDatabase().getCollection("friendships").deleteMany(eq("friendId", target.getPlayerUid())); DatabaseManager.getGameDatabase().getCollection("friendships").deleteMany(eq("friendId", target.getPlayerUid()));
// Delete the player. // Delete the player.
DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("id", target.getPlayerUid())).delete(); DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("id", target.getPlayerUid())).delete();
// Finally, delete the account itself. // Finally, delete the account itself.
DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("id", target.getId())).delete(); DatabaseManager.getGameDatastore().find(Account.class).filter(Filters.eq("id", target.getId())).delete();
} }
public static List<Player> getAllPlayers() { public static List<Player> getAllPlayers() {
return DatabaseManager.getDatastore().find(Player.class).stream().toList(); return DatabaseManager.getGameDatastore().find(Player.class).stream().toList();
} }
public static Player getPlayerById(int id) { public static Player getPlayerById(int id) {
return DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("_id", id)).first(); return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", id)).first();
} }
public static boolean checkPlayerExists(int id) { public static boolean checkPlayerExists(int id) {
return DatabaseManager.getDatastore().find(Player.class).filter(Filters.eq("_id", id)).first() != null; return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", id)).first() != null;
} }
public static synchronized Player createPlayer(Player character, int reservedId) { public static synchronized Player createPlayer(Player character, int reservedId) {
@ -154,7 +154,7 @@ public final class DatabaseHelper {
character.setUid(id); character.setUid(id);
} }
// Save to database // Save to database
DatabaseManager.getDatastore().save(character); DatabaseManager.getGameDatastore().save(character);
return character; return character;
} }
@ -173,48 +173,48 @@ public final class DatabaseHelper {
} }
public static void savePlayer(Player character) { public static void savePlayer(Player character) {
DatabaseManager.getDatastore().save(character); DatabaseManager.getGameDatastore().save(character);
} }
public static void saveAvatar(Avatar avatar) { public static void saveAvatar(Avatar avatar) {
DatabaseManager.getDatastore().save(avatar); DatabaseManager.getGameDatastore().save(avatar);
} }
public static List<Avatar> getAvatars(Player player) { public static List<Avatar> getAvatars(Player player) {
return DatabaseManager.getDatastore().find(Avatar.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(Avatar.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList();
} }
public static void saveItem(GameItem item) { public static void saveItem(GameItem item) {
DatabaseManager.getDatastore().save(item); DatabaseManager.getGameDatastore().save(item);
} }
public static boolean deleteItem(GameItem item) { public static boolean deleteItem(GameItem item) {
DeleteResult result = DatabaseManager.getDatastore().delete(item); DeleteResult result = DatabaseManager.getGameDatastore().delete(item);
return result.wasAcknowledged(); return result.wasAcknowledged();
} }
public static List<GameItem> getInventoryItems(Player player) { public static List<GameItem> getInventoryItems(Player player) {
return DatabaseManager.getDatastore().find(GameItem.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(GameItem.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList();
} }
public static List<Friendship> getFriends(Player player) { public static List<Friendship> getFriends(Player player) {
return DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList();
} }
public static List<Friendship> getReverseFriends(Player player) { public static List<Friendship> getReverseFriends(Player player) {
return DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.eq("friendId", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.eq("friendId", player.getUid())).stream().toList();
} }
public static void saveFriendship(Friendship friendship) { public static void saveFriendship(Friendship friendship) {
DatabaseManager.getDatastore().save(friendship); DatabaseManager.getGameDatastore().save(friendship);
} }
public static void deleteFriendship(Friendship friendship) { public static void deleteFriendship(Friendship friendship) {
DatabaseManager.getDatastore().delete(friendship); DatabaseManager.getGameDatastore().delete(friendship);
} }
public static Friendship getReverseFriendship(Friendship friendship) { public static Friendship getReverseFriendship(Friendship friendship) {
return DatabaseManager.getDatastore().find(Friendship.class).filter(Filters.and( return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.and(
Filters.eq("ownerId", friendship.getFriendId()), Filters.eq("ownerId", friendship.getFriendId()),
Filters.eq("friendId", friendship.getOwnerId()) Filters.eq("friendId", friendship.getOwnerId())
)).first(); )).first();
@ -225,7 +225,7 @@ public final class DatabaseHelper {
} }
public static List<GachaRecord> getGachaRecords(int ownerId, int page, int gachaType, int pageSize){ public static List<GachaRecord> getGachaRecords(int ownerId, int page, int gachaType, int pageSize){
return DatabaseManager.getDatastore().find(GachaRecord.class).filter( return DatabaseManager.getGameDatastore().find(GachaRecord.class).filter(
Filters.eq("ownerId", ownerId), Filters.eq("ownerId", ownerId),
Filters.eq("gachaType", gachaType) Filters.eq("gachaType", gachaType)
).iterator(new FindOptions() ).iterator(new FindOptions()
@ -240,7 +240,7 @@ public final class DatabaseHelper {
} }
public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType, int pageSize){ public static long getGachaRecordsMaxPage(int ownerId, int page, int gachaType, int pageSize){
long count = DatabaseManager.getDatastore().find(GachaRecord.class).filter( long count = DatabaseManager.getGameDatastore().find(GachaRecord.class).filter(
Filters.eq("ownerId", ownerId), Filters.eq("ownerId", ownerId),
Filters.eq("gachaType", gachaType) Filters.eq("gachaType", gachaType)
).count(); ).count();
@ -248,31 +248,31 @@ public final class DatabaseHelper {
} }
public static void saveGachaRecord(GachaRecord gachaRecord){ public static void saveGachaRecord(GachaRecord gachaRecord){
DatabaseManager.getDatastore().save(gachaRecord); DatabaseManager.getGameDatastore().save(gachaRecord);
} }
public static List<Mail> getAllMail(Player player) { public static List<Mail> getAllMail(Player player) {
return DatabaseManager.getDatastore().find(Mail.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(Mail.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList();
} }
public static void saveMail(Mail mail) { public static void saveMail(Mail mail) {
DatabaseManager.getDatastore().save(mail); DatabaseManager.getGameDatastore().save(mail);
} }
public static boolean deleteMail(Mail mail) { public static boolean deleteMail(Mail mail) {
DeleteResult result = DatabaseManager.getDatastore().delete(mail); DeleteResult result = DatabaseManager.getGameDatastore().delete(mail);
return result.wasAcknowledged(); return result.wasAcknowledged();
} }
public static List<GameMainQuest> getAllQuests(Player player) { public static List<GameMainQuest> getAllQuests(Player player) {
return DatabaseManager.getDatastore().find(GameMainQuest.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(GameMainQuest.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList();
} }
public static void saveQuest(GameMainQuest quest) { public static void saveQuest(GameMainQuest quest) {
DatabaseManager.getDatastore().save(quest); DatabaseManager.getGameDatastore().save(quest);
} }
public static boolean deleteQuest(GameMainQuest quest) { public static boolean deleteQuest(GameMainQuest quest) {
return DatabaseManager.getDatastore().delete(quest).wasAcknowledged(); return DatabaseManager.getGameDatastore().delete(quest).wasAcknowledged();
} }
} }

View File

@ -25,7 +25,7 @@ import emu.grasscutter.game.quest.GameQuest;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.*;
public final class DatabaseManager { public final class DatabaseManager {
private static Datastore datastore; private static Datastore gameDatastore;
private static Datastore dispatchDatastore; private static Datastore dispatchDatastore;
private static final Class<?>[] mappedClasses = new Class<?>[] { private static final Class<?>[] mappedClasses = new Class<?>[] {
@ -33,12 +33,12 @@ public final class DatabaseManager {
GachaRecord.class, Mail.class, GameMainQuest.class GachaRecord.class, Mail.class, GameMainQuest.class
}; };
public static Datastore getDatastore() { public static Datastore getGameDatastore() {
return datastore; return gameDatastore;
} }
public static MongoDatabase getDatabase() { public static MongoDatabase getGameDatabase() {
return getDatastore().getDatabase(); return getGameDatastore().getDatabase();
} }
// Yes. I very dislike this method. However, this will be good for now. // Yes. I very dislike this method. However, this will be good for now.
@ -47,42 +47,42 @@ public final class DatabaseManager {
if(SERVER.runMode == ServerRunMode.GAME_ONLY) { if(SERVER.runMode == ServerRunMode.GAME_ONLY) {
return dispatchDatastore; return dispatchDatastore;
} else { } else {
return datastore; return gameDatastore;
} }
} }
public static void initialize() { public static void initialize() {
// Initialize // Initialize
MongoClient mongoClient = MongoClients.create(DATABASE.connectionUri); MongoClient gameMongoClient = MongoClients.create(DATABASE.game.connectionUri);
// Set mapper options. // Set mapper options.
MapperOptions mapperOptions = MapperOptions.builder() MapperOptions mapperOptions = MapperOptions.builder()
.storeEmpties(true).storeNulls(false).build(); .storeEmpties(true).storeNulls(false).build();
// Create data store. // Create data store.
datastore = Morphia.createDatastore(mongoClient, DATABASE.collection, mapperOptions); gameDatastore = Morphia.createDatastore(gameMongoClient, DATABASE.game.collection, mapperOptions);
// Map classes. // Map classes.
datastore.getMapper().map(mappedClasses); gameDatastore.getMapper().map(mappedClasses);
// Ensure indexes // Ensure indexes
try { try {
datastore.ensureIndexes(); gameDatastore.ensureIndexes();
} catch (MongoCommandException exception) { } catch (MongoCommandException exception) {
Grasscutter.getLogger().info("Mongo index error: ", exception); Grasscutter.getLogger().info("Mongo index error: ", exception);
// Duplicate index error // Duplicate index error
if (exception.getCode() == 85) { if (exception.getCode() == 85) {
// Drop all indexes and re add them // Drop all indexes and re add them
MongoIterable<String> collections = datastore.getDatabase().listCollectionNames(); MongoIterable<String> collections = gameDatastore.getDatabase().listCollectionNames();
for (String name : collections) { for (String name : collections) {
datastore.getDatabase().getCollection(name).dropIndexes(); gameDatastore.getDatabase().getCollection(name).dropIndexes();
} }
// Add back indexes // Add back indexes
datastore.ensureIndexes(); gameDatastore.ensureIndexes();
} }
} }
if(SERVER.runMode == ServerRunMode.GAME_ONLY) { if(SERVER.runMode == ServerRunMode.GAME_ONLY) {
MongoClient dispatchMongoClient = MongoClients.create(GAME_OPTIONS.databaseInfo.connectionUri); MongoClient dispatchMongoClient = MongoClients.create(DATABASE.server.connectionUri);
dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, GAME_OPTIONS.databaseInfo.collection); dispatchDatastore = Morphia.createDatastore(dispatchMongoClient, DATABASE.server.collection);
// Ensure indexes for dispatch server // Ensure indexes for dispatch server
try { try {
@ -104,14 +104,14 @@ public final class DatabaseManager {
} }
public static synchronized int getNextId(Class<?> c) { public static synchronized int getNextId(Class<?> c) {
DatabaseCounter counter = getDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first(); DatabaseCounter counter = getGameDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first();
if (counter == null) { if (counter == null) {
counter = new DatabaseCounter(c.getSimpleName()); counter = new DatabaseCounter(c.getSimpleName());
} }
try { try {
return counter.getNextId(); return counter.getNextId();
} finally { } finally {
getDatastore().save(counter); getGameDatastore().save(counter);
} }
} }

View File

@ -102,6 +102,11 @@ public class GachaBanner {
+ lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":"
+ lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort)
+ "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType;
String details = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://"
+ lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":"
+ lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort)
+ "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType;
// Grasscutter.getLogger().info("record = " + record); // Grasscutter.getLogger().info("record = " + record);
GachaInfo.Builder info = GachaInfo.newBuilder() GachaInfo.Builder info = GachaInfo.newBuilder()
.setGachaType(this.getGachaType()) .setGachaType(this.getGachaType())
@ -112,8 +117,8 @@ public class GachaBanner {
.setCostItemNum(1) .setCostItemNum(1)
.setGachaPrefabPath(this.getPrefabPath()) .setGachaPrefabPath(this.getPrefabPath())
.setGachaPreviewPrefabPath(this.getPreviewPrefabPath()) .setGachaPreviewPrefabPath(this.getPreviewPrefabPath())
.setGachaProbUrl(record) .setGachaProbUrl(details)
.setGachaProbUrlOversea(record) .setGachaProbUrlOversea(details)
.setGachaRecordUrl(record) .setGachaRecordUrl(record)
.setGachaRecordUrlOversea(record) .setGachaRecordUrlOversea(record)
.setTenCostItemId(this.getCostItem()) .setTenCostItemId(this.getCostItem())

View File

@ -65,7 +65,23 @@ public class GachaManager {
public Int2ObjectMap<GachaBanner> getGachaBanners() { public Int2ObjectMap<GachaBanner> getGachaBanners() {
return gachaBanners; return gachaBanners;
} }
public int[] getYellowAvatars() {
return this.yellowAvatars;
}
public int[] getYellowWeapons() {
return this.yellowWeapons;
}
public int[] getPurpleAvatars() {
return this.purpleAvatars;
}
public int[] getPurpleWeapons() {
return this.purpleWeapons;
}
public int[] getBlueWeapons() {
return this.blueWeapons;
}
public int randomRange(int min, int max) { public int randomRange(int min, int max) {
return ThreadLocalRandom.current().nextInt(max - min + 1) + min; return ThreadLocalRandom.current().nextInt(max - min + 1) + min;
} }

View File

@ -7,6 +7,7 @@ import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@ -90,6 +91,8 @@ public final class PluginManager {
fileReader.close(); // Close the file reader. fileReader.close(); // Close the file reader.
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class."); Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class.");
} catch (FileNotFoundException ignored) {
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " lacks a valid config file.");
} }
} catch (Exception exception) { } catch (Exception exception) {
Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), exception); Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), exception);

View File

@ -16,6 +16,7 @@ import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import emu.grasscutter.server.dispatch.authentication.AuthenticationHandler; import emu.grasscutter.server.dispatch.authentication.AuthenticationHandler;
import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler; import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler;
import emu.grasscutter.server.dispatch.http.GachaDetailsHandler;
import emu.grasscutter.server.dispatch.http.GachaRecordHandler; import emu.grasscutter.server.dispatch.http.GachaRecordHandler;
import emu.grasscutter.server.dispatch.json.*; import emu.grasscutter.server.dispatch.json.*;
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData;
@ -117,7 +118,7 @@ public final class DispatchServer {
.setTitle(DISPATCH_INFO.defaultName) .setTitle(DISPATCH_INFO.defaultName)
.setType("DEV_PUBLIC") .setType("DEV_PUBLIC")
.setDispatchUrl( .setDispatchUrl(
"http" + (DISPATCH_ENCRYPTION.useEncryption ? "s" : "") + "://" "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":"
+ lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort)
+ "/query_cur_region/" + defaultServerName) + "/query_cur_region/" + defaultServerName)
@ -150,7 +151,7 @@ public final class DispatchServer {
.setTitle(regionInfo.Title) .setTitle(regionInfo.Title)
.setType("DEV_PUBLIC") .setType("DEV_PUBLIC")
.setDispatchUrl( .setDispatchUrl(
"http" + (DISPATCH_ENCRYPTION.useEncryption ? "s" : "") + "://" "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":"
+ lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort)
+ "/query_cur_region/" + regionInfo.Name) + "/query_cur_region/" + regionInfo.Name)
@ -455,6 +456,9 @@ public final class DispatchServer {
httpServer.raw().config.addSinglePageRoot("/gacha/mappings", gachaMappingsPath, Location.EXTERNAL); httpServer.raw().config.addSinglePageRoot("/gacha/mappings", gachaMappingsPath, Location.EXTERNAL);
// gacha details
httpServer.get("/gacha/details", new GachaDetailsHandler());
// static file support for plugins // static file support for plugins
httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files

View File

@ -12,5 +12,12 @@ public interface AuthenticationHandler {
void handleRegister(Request req, Response res); void handleRegister(Request req, Response res);
void handleChangePassword(Request req, Response res); void handleChangePassword(Request req, Response res);
/**
* Other plugins may need to verify a user's identity using details from handleLogin()
* @param details The user's unique one-time token that needs to be verified
* @return If the verification was successful
*/
boolean verifyUser(String details);
LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData); LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData);
} }

View File

@ -28,6 +28,12 @@ public class DefaultAuthenticationHandler implements AuthenticationHandler {
res.send("Authentication is not available with the default authentication method"); res.send("Authentication is not available with the default authentication method");
} }
@Override
public boolean verifyUser(String details) {
Grasscutter.getLogger().info(translate("dispatch.authentication.default_unable_to_verify"));
return false;
}
@Override @Override
public LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData) { public LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData) {
LoginResultJson responseData = new LoginResultJson(); LoginResultJson responseData = new LoginResultJson();

View File

@ -0,0 +1,92 @@
package emu.grasscutter.server.dispatch.http;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.gacha.GachaBanner;
import emu.grasscutter.game.gacha.GachaManager;
import emu.grasscutter.game.gacha.GachaBanner.BannerType;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.http.HttpContextHandler;
import express.http.Request;
import express.http.Response;
import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.Configuration.*;
public final class GachaDetailsHandler implements HttpContextHandler {
private final String render_template;
public GachaDetailsHandler() {
File template = new File(Utils.toFilePath(DATA("/gacha_details.html")));
this.render_template = template.exists() ? new String(FileUtils.read(template)) : null;
}
@Override
public void handle(Request req, Response res) throws IOException {
String response = this.render_template;
// Get player info (for langauge).
String sessionKey = req.query("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
Player player = Grasscutter.getGameServer().getPlayerByUid(account.getPlayerUid());
// If the template was not loaded, return an error.
if (this.render_template == null) {
res.send(translate(player, "gacha.details.template_missing"));
return;
}
// Add translated title etc. to the page.
response = response.replace("{{TITLE}}", translate(player, "gacha.details.title"));
response = response.replace("{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars"));
response = response.replace("{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars"));
response = response.replace("{{AVAILABLE_THREE_STARS}}", translate(player, "gacha.details.available_three_stars"));
response = response.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
// Get the banner info for the banner we want.
int gachaType = Integer.parseInt(req.query("gachaType"));
GachaManager manager = Grasscutter.getGameServer().getGachaManager();
GachaBanner banner = manager.getGachaBanners().get(gachaType);
// Add 5-star items.
Set<String> fiveStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getRateUpItems1()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
if (banner.getBannerType() == BannerType.STANDARD || banner.getBannerType() == BannerType.EVENT) {
Arrays.stream(manager.getYellowAvatars()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
}
if (banner.getBannerType() == BannerType.STANDARD || banner.getBannerType() == BannerType.WEAPON) {
Arrays.stream(manager.getYellowWeapons()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
}
response = response.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]");
// Add 4-star items.
Set<String> fourStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getRateUpItems2()).forEach(i -> fourStarItems.add(Integer.toString(i)));
Arrays.stream(manager.getPurpleAvatars()).forEach(i -> fourStarItems.add(Integer.toString(i)));
Arrays.stream(manager.getPurpleWeapons()).forEach(i -> fourStarItems.add(Integer.toString(i)));
response = response.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]");
// Add 3-star items.
Set<String> threeStarItems = new LinkedHashSet<>();
Arrays.stream(manager.getBlueWeapons()).forEach(i -> threeStarItems.add(Integer.toString(i)));
response = response.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
// Done.
res.send(response);
}
}

View File

@ -2,6 +2,8 @@ package emu.grasscutter.utils;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.Grasscutter.ServerRunMode;
import java.io.FileReader; import java.io.FileReader;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@ -15,7 +17,7 @@ import static emu.grasscutter.Grasscutter.config;
*/ */
public class ConfigContainer { public class ConfigContainer {
private static int version() { private static int version() {
return 1; return 2;
} }
/** /**
@ -69,8 +71,13 @@ public class ConfigContainer {
/* Option containers. */ /* Option containers. */
public static class Database { public static class Database {
public String connectionUri = "mongodb://localhost:27017"; public DataStore server = new DataStore();
public String collection = "grasscutter"; public DataStore game = new DataStore();
public static class DataStore {
public String connectionUri = "mongodb://localhost:27017";
public String collection = "grasscutter";
}
} }
public static class Structure { public static class Structure {
@ -86,8 +93,8 @@ public class ConfigContainer {
} }
public static class Server { public static class Server {
public Grasscutter.ServerDebugMode debugLevel = Grasscutter.ServerDebugMode.NONE; public ServerDebugMode debugLevel = ServerDebugMode.NONE;
public Grasscutter.ServerRunMode runMode = Grasscutter.ServerRunMode.HYBRID; public ServerRunMode runMode = ServerRunMode.HYBRID;
public Dispatch dispatch = new Dispatch(); public Dispatch dispatch = new Dispatch();
public Game game = new Game(); public Game game = new Game();
@ -112,7 +119,7 @@ public class ConfigContainer {
public int bindPort = 443; public int bindPort = 443;
/* This is the port used in URLs. */ /* This is the port used in URLs. */
public int accessPort = 443; public int accessPort = 0;
public Encryption encryption = new Encryption(); public Encryption encryption = new Encryption();
public Policies policies = new Policies(); public Policies policies = new Policies();
@ -128,7 +135,7 @@ public class ConfigContainer {
public int bindPort = 22102; public int bindPort = 22102;
/* This is the port used in the default region. */ /* This is the port used in the default region. */
public int accessPort = 22102; public int accessPort = 0;
public GameOptions gameOptions = new GameOptions(); public GameOptions gameOptions = new GameOptions();
public JoinOptions joinOptions = new JoinOptions(); public JoinOptions joinOptions = new JoinOptions();
@ -155,16 +162,14 @@ public class ConfigContainer {
} }
public static class GameOptions { public static class GameOptions {
public GameOptions.InventoryLimits inventoryLimits = new GameOptions.InventoryLimits(); public InventoryLimits inventoryLimits = new InventoryLimits();
public GameOptions.AvatarLimits avatarLimits = new GameOptions.AvatarLimits(); public AvatarLimits avatarLimits = new AvatarLimits();
public int worldEntityLimit = 1000; // Unenforced. TODO: Implement. public int worldEntityLimit = 1000; // Unenforced. TODO: Implement.
public boolean watchGachaConfig = false; public boolean watchGachaConfig = false;
public boolean enableShopItems = true; public boolean enableShopItems = true;
public boolean staminaUsage = true; public boolean staminaUsage = true;
public GameOptions.Rates rates = new GameOptions.Rates(); public Rates rates = new Rates();
public Database databaseInfo = new Database();
public static class InventoryLimits { public static class InventoryLimits {
public int weapons = 2000; public int weapons = 2000;

View File

@ -160,7 +160,9 @@ public final class Language {
JsonObject object = this.languageData; JsonObject object = this.languageData;
int index = 0; int index = 0;
String result = "This value does not exist. Please report this to the Discord: " + key; String valueNotFoundPattern = "This value does not exist. Please report this to the Discord: ";
String result = valueNotFoundPattern + key;
boolean isValueFound = false;
while (true) { while (true) {
if(index == keys.length) break; if(index == keys.length) break;
@ -171,10 +173,18 @@ public final class Language {
if(element.isJsonObject()) if(element.isJsonObject())
object = element.getAsJsonObject(); object = element.getAsJsonObject();
else { else {
isValueFound = true;
result = element.getAsString(); break; result = element.getAsString(); break;
} }
} else break; } else break;
} }
if (!isValueFound && !languageCode.equals("en-US")) {
var englishValue = Grasscutter.getLanguage("en-US").get(key);
if (!englishValue.contains(valueNotFoundPattern)) {
result += "\nhere is english version:\n" + englishValue;
}
}
this.cachedTranslations.put(key, result); return result; this.cachedTranslations.put(key, result); return result;
} }

View File

@ -16,6 +16,9 @@
"no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.", "no_keystore_error": "[Dispatch] No SSL cert found! Falling back to HTTP server.",
"default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json." "default_password": "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."
}, },
"authentication": {
"default_unable_to_verify": "[Authentication] Something called the verifyUser method which is unavailable in the default authentication handler"
},
"no_commands_error": "Commands are not supported in dispatch only mode.", "no_commands_error": "Commands are not supported in dispatch only mode.",
"unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s", "unhandled_request_error": "[Dispatch] Potential unhandled %s request: %s",
"account": { "account": {
@ -45,7 +48,8 @@
"run_mode_error": "Invalid server run mode: %s.", "run_mode_error": "Invalid server run mode: %s.",
"run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...", "run_mode_help": "Server run mode must be 'HYBRID', 'DISPATCH_ONLY', or 'GAME_ONLY'. Unable to start Grasscutter...",
"create_resources": "Creating resources folder...", "create_resources": "Creating resources folder...",
"resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder." "resources_error": "Place a copy of 'BinOutput' and 'ExcelBinOutput' in the resources folder.",
"version": "Grasscutter version: %s-%s"
} }
}, },
"commands": { "commands": {
@ -356,5 +360,14 @@
"resetshop": { "resetshop": {
"description": "reset shop" "description": "reset shop"
} }
},
"gacha": {
"details": {
"title": "Banner Details",
"available_five_stars": "Available 5-star Items",
"available_four_stars": "Available 4-star Items",
"available_three_stars": "Available 3-star Items",
"template_missing": "data/gacha_details.html is missing."
}
} }
} }

View File

@ -45,7 +45,8 @@
"run_mode_error": "Błędny tryb pracy serwera: %s.", "run_mode_error": "Błędny tryb pracy serwera: %s.",
"run_mode_help": "Tryb pracy serwera musi być ustawiony na 'HYBRID', 'DISPATCH_ONLY', lub 'GAME_ONLY'. Nie można wystartować Grasscutter...", "run_mode_help": "Tryb pracy serwera musi być ustawiony na 'HYBRID', 'DISPATCH_ONLY', lub 'GAME_ONLY'. Nie można wystartować Grasscutter...",
"create_resources": "Tworzenie folderu resources...", "create_resources": "Tworzenie folderu resources...",
"resources_error": "Umieść kopię 'BinOutput' i 'ExcelBinOutput' w folderze resources." "resources_error": "Umieść kopię 'BinOutput' i 'ExcelBinOutput' w folderze resources.",
"version": "Grasscutter versión: %s-%s"
} }
}, },
"commands": { "commands": {
@ -301,5 +302,14 @@
"resetshop": { "resetshop": {
"description": "zresetuj sklep" "description": "zresetuj sklep"
} }
},
"gacha": {
"details": {
"title": "Banner Details",
"available_five_stars": "Available 5-star Items",
"available_four_stars": "Available 4-star Items",
"available_three_stars": "Available 3-star Items",
"template_missing": "data/gacha_details.html is missing."
}
} }
} }

View File

@ -20,7 +20,7 @@
"unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s", "unhandled_request_error": "[Dispatch] 潜在的未处理请求:%s %s",
"account": { "account": {
"login_attempt": "[Dispatch] 客户端 %s 正在尝试登录", "login_attempt": "[Dispatch] 客户端 %s 正在尝试登录",
"login_success": "[Dispatch] 客户端 %s 已登录UID为 %s", "login_success": "[Dispatch] 客户端 %s 已登录UID 为 %s",
"login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录", "login_token_attempt": "[Dispatch] 客户端 %s 正在尝试使用 token 登录",
"login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败", "login_token_error": "[Dispatch] 客户端 %s 使用 token 登录失败",
"login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录UID 为 %s", "login_token_success": "[Dispatch] 客户端 %s 已通过 token 登录UID 为 %s",
@ -45,7 +45,8 @@
"run_mode_error": "无效的服务器运行模式:%s。", "run_mode_error": "无效的服务器运行模式:%s。",
"run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...", "run_mode_help": "服务器运行模式必须为 HYBRID、DISPATCH_ONLY 或 GAME_ONLY。Grasscutter 启动失败...",
"create_resources": "正在创建 resources 目录...", "create_resources": "正在创建 resources 目录...",
"resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。" "resources_error": "请将 BinOutput 和 ExcelBinOutput 复制到 resources 目录。",
"version": "Grasscutter版本: %s-%s"
} }
}, },
"commands": { "commands": {
@ -55,7 +56,7 @@
"permission_error": "哼哼哼!你没有执行此命令的权限!请联系服务器管理员解决!", "permission_error": "哼哼哼!你没有执行此命令的权限!请联系服务器管理员解决!",
"console_execute_error": "此命令只能在服务器控制台执行呐~", "console_execute_error": "此命令只能在服务器控制台执行呐~",
"player_execute_error": "此命令只能在游戏内执行哦~", "player_execute_error": "此命令只能在游戏内执行哦~",
"command_exist_error": "这条命令……好像找不到呢?。", "command_exist_error": "这条命令...好像找不到呢?",
"no_description_specified": "没有指定说明", "no_description_specified": "没有指定说明",
"invalid": { "invalid": {
"amount": "无效的数量。", "amount": "无效的数量。",
@ -96,20 +97,20 @@
"create": "已创建账号UID 为 %s。", "create": "已创建账号UID 为 %s。",
"delete": "账号已删除。", "delete": "账号已删除。",
"no_account": "账号不存在。", "no_account": "账号不存在。",
"command_usage": "用法account <create|delete> <用户名> [uid]", "command_usage": "用法account <create|delete> <用户名> [UID]",
"description": "创建或删除账号" "description": "创建或删除账号"
}, },
"broadcast": { "broadcast": {
"command_usage": "用法broadcast <消息>", "command_usage": "用法broadcast <消息>",
"message_sent": "公告已发送。", "message_sent": "公告已发送。",
"description": "向所有玩家发送公告" "description": "向所有玩家发送公告"
}, },
"changescene": { "changescene": {
"usage": "用法changescene <场景ID>", "usage": "用法changescene <场景ID>",
"already_in_scene": "你已经在这个场景中了。", "already_in_scene": "你已经在这个场景中了。",
"success": "已切换至场景 %s。", "success": "已切换至场景 %s。",
"exists_error": "此场景不存在。", "exists_error": "此场景不存在。",
"description": "切换指定场景" "description": "切换指定场景"
}, },
"clear": { "clear": {
"command_usage": "用法clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料", "command_usage": "用法clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料",
@ -120,32 +121,32 @@
"displays": "已清空 %s 的屏幕。", "displays": "已清空 %s 的屏幕。",
"virtuals": "已清除 %s 的所有货币和经验值。", "virtuals": "已清除 %s 的所有货币和经验值。",
"everything": "已清除 %s 的所有物品。", "everything": "已清除 %s 的所有物品。",
"description": "从你的背包中删除所有未装备且已解锁的物品,包括稀有物品" "description": "从你的背包中删除所有未装备且已解锁的物品,包括稀有物品"
}, },
"coop": { "coop": {
"usage": "用法coop <玩家ID> <目标玩家ID>", "usage": "用法coop <玩家ID> <目标玩家ID>",
"success": "已强制传送 %s 到 %s 的世界", "success": "已强制传送 %s 到 %s 的世界",
"description": "强制传送指定用户到他人的世界" "description": "强制传送指定用户到他人的世界"
}, },
"enter_dungeon": { "enter_dungeon": {
"usage": "用法enterdungeon <秘境ID>", "usage": "用法enterdungeon <秘境ID>",
"changed": "已进入秘境 %s", "changed": "已进入秘境 %s",
"not_found_error": "此秘境不存在。", "not_found_error": "此秘境不存在。",
"in_dungeon_error": "你已经在秘境中了。", "in_dungeon_error": "你已经在秘境中了。",
"description": "进入指定秘境" "description": "进入指定秘境"
}, },
"giveAll": { "giveAll": {
"usage": "用法giveall [玩家] [数量]", "usage": "用法giveall [玩家] [数量]",
"started": "正在给予全部物品...", "started": "正在给予全部物品...",
"success": "已给予 %s 全部物品。", "success": "已给予 %s 全部物品。",
"invalid_amount_or_playerId": "无效的数量/玩家ID。", "invalid_amount_or_playerId": "无效的数量/玩家ID。",
"description": "给予所有物品" "description": "给予所有物品"
}, },
"giveArtifact": { "giveArtifact": {
"usage": "用法giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]", "usage": "用法giveart|gart [玩家] <圣遗物ID> <主词条ID> [<副词条ID>[,<强化次数>]]... [等级]",
"id_error": "无效的圣遗物ID。", "id_error": "无效的圣遗物ID。",
"success": "已将 %s 给予 %s。", "success": "已将 %s 给予 %s。",
"description": "给予指定圣遗物" "description": "给予指定圣遗物"
}, },
"giveChar": { "giveChar": {
"usage": "用法givechar <玩家> <角色ID|角色名> [数量]", "usage": "用法givechar <玩家> <角色ID|角色名> [数量]",
@ -153,50 +154,50 @@
"invalid_avatar_id": "无效的角色ID。", "invalid_avatar_id": "无效的角色ID。",
"invalid_avatar_level": "无效的角色等级。", "invalid_avatar_level": "无效的角色等级。",
"invalid_avatar_or_player_id": "无效的角色ID/玩家ID。", "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。",
"description": "给予指定角色" "description": "给予指定角色"
}, },
"give": { "give": {
"usage": "用法give <玩家> <物品ID|物品名> [数量] [等级] [精炼等级]", "usage": "用法give <玩家> <物品ID|物品名> [数量] [等级] [精炼等级]",
"refinement_only_applicable_weapons": "只有武器可以设置精炼等级。", "refinement_only_applicable_weapons": "只有武器可以设置精炼等级。",
"refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。", "refinement_must_between_1_and_5": "精炼等级必须在 1 到 5 之间。",
"given": "已将 %s 个 %s 给予 %s。", "given": "已将 %s 个 %s 给予 %s。",
"given_with_level_and_refinement": "已将 %s (等级 %s, 精炼 %s) %s 个给予 %s", "given_with_level_and_refinement": "已将 %s (等级 %s, 精炼 %s) %s 个给予 %s",
"given_level": "已将 %s (等级 %s) %s 个给予 %s", "given_level": "已将 %s (等级 %s) %s 个给予 %s",
"description": "给予指定物品" "description": "给予指定物品"
}, },
"godmode": { "godmode": {
"success": "%s 的无敌模式已被设置为 %s。", "success": "%s 的上帝模式已被设置为 %s。",
"description": "防止你受到伤害" "description": "防止你受到伤害"
}, },
"heal": { "heal": {
"success": "已经治疗所有角色。", "success": "已经治疗所有角色。",
"description": "治疗当前队伍的角色" "description": "治疗当前队伍的角色"
}, },
"kick": { "kick": {
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出", "player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出",
"server_kick_player": "正在踢出玩家 [%s:%s]", "server_kick_player": "正在踢出玩家 [%s:%s]...",
"description": "从服务器内踢出指定玩家" "description": "从服务器内踢出指定玩家"
}, },
"kill": { "kill": {
"usage": "用法killall [玩家UID] [场景ID]", "usage": "用法killall [玩家UID] [场景ID]",
"scene_not_found_in_player_world": "未在玩家世界中找到此场景", "scene_not_found_in_player_world": "未在玩家世界中找到此场景",
"kill_monsters_in_scene": "已杀死场景 %s 中的 %s 个怪物。", "kill_monsters_in_scene": "已杀死场景 %s 中的 %s 个怪物。",
"description": "杀死所有怪物" "description": "杀死所有怪物"
}, },
"killCharacter": { "killCharacter": {
"usage": "用法:/killcharacter [玩家ID]", "usage": "用法:/killcharacter [玩家ID]",
"success": "已杀死 %s 当前角色。", "success": "已杀死 %s 当前角色。",
"description": "杀死当前角色" "description": "杀死当前角色"
}, },
"language": { "language": {
"current_language": "当前语言是: %s", "current_language": "当前语言是: %s",
"language_changed": "语言切换至: %s", "language_changed": "语言切换至: %s",
"language_not_found": "目前服务端没有这种语言: %s", "language_not_found": "目前服务端没有这种语言: %s",
"description": "显示或切换当前语言" "description": "显示或切换当前语言"
}, },
"list": { "list": {
"success": "目前在线人数:%s", "success": "目前在线人数:%s",
"description": "查看所有玩家" "description": "查看所有玩家"
}, },
"permission": { "permission": {
"usage": "用法permission <add|remove> <用户名> <权限>", "usage": "用法permission <add|remove> <用户名> <权限>",
@ -205,25 +206,25 @@
"remove": "权限已移除。", "remove": "权限已移除。",
"not_have_error": "此玩家未拥有权限!", "not_have_error": "此玩家未拥有权限!",
"account_error": "账号不存在。", "account_error": "账号不存在。",
"description": "添加或移除指定玩家的权限" "description": "添加或移除指定玩家的权限"
}, },
"position": { "position": {
"success": "坐标:%s, %s, %s\n场景ID%s", "success": "坐标:%s, %s, %s\n场景ID%s",
"description": "获取所在位置" "description": "获取所在位置"
}, },
"reload": { "reload": {
"reload_start": "正在重载配置文件和数据。", "reload_start": "正在重载配置文件和数据。",
"reload_done": "重载完成。", "reload_done": "重载完成。",
"description": "重载配置文件和数据" "description": "重载配置文件和数据"
}, },
"resetConst": { "resetConst": {
"reset_all": "重置所有角色的命座。", "reset_all": "重置所有角色的命座。",
"success": "已重置 %s 的命座,重新登录后生效。", "success": "已重置 %s 的命座,重新登录后生效。",
"description": "重置当前角色的命之座,执行命令后需重新登录以生效" "description": "重置当前角色的命之座,执行命令后需重新登录以生效"
}, },
"resetShopLimit": { "resetShopLimit": {
"usage": "用法:/resetshop <玩家ID>", "usage": "用法:/resetshop <玩家ID>",
"description": "重置所选玩家的商店刷新时间" "description": "重置所选玩家的商店刷新时间"
}, },
"sendMail": { "sendMail": {
"usage": "用法give [玩家] <物品ID|物品名称> [数量]", "usage": "用法give [玩家] <物品ID|物品名称> [数量]",
@ -246,19 +247,19 @@
"sender": "<发件人>", "sender": "<发件人>",
"arguments": "<物品ID|物品名称|finish> [数量] [等级]", "arguments": "<物品ID|物品名称|finish> [数量] [等级]",
"error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。", "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。",
"description": "向指定用户发送邮件。此命令的用法可根据附加的参数而变化" "description": "向指定用户发送邮件。此命令的用法可根据附加的参数而变化"
}, },
"sendMessage": { "sendMessage": {
"usage": "用法sendmessage <玩家> <消息>", "usage": "用法sendmessage <玩家> <消息>",
"success": "消息已发送。", "success": "消息已发送。",
"description": "向指定玩家发送消息" "description": "向指定玩家发送消息"
}, },
"setFetterLevel": { "setFetterLevel": {
"usage": "用法setfetterlevel <好感度等级>", "usage": "用法setfetterlevel <好感度等级>",
"range_error": "好感度等级必须在 0 到 10 之间。", "range_error": "好感度等级必须在 0 到 10 之间。",
"success": "好感度已设置为 %s 级", "success": "好感度已设置为 %s 级",
"level_error": "无效的好感度等级。", "level_error": "无效的好感度等级。",
"description": "设置当前角色的好感度等级" "description": "设置当前角色的好感度等级"
}, },
"setStats": { "setStats": {
"usage_console": "用法setstats|stats @<UID> <属性> <数值>", "usage_console": "用法setstats|stats @<UID> <属性> <数值>",
@ -270,27 +271,27 @@
"set_self": "%s 已设为 %s。", "set_self": "%s 已设为 %s。",
"set_for_uid": "将 %s (来自 %s) 设置为 %s。", "set_for_uid": "将 %s (来自 %s) 设置为 %s。",
"set_max_hp": "最大生命值已设为 %s。", "set_max_hp": "最大生命值已设为 %s。",
"description": "设置当前角色的属性" "description": "设置当前角色的属性"
}, },
"setWorldLevel": { "setWorldLevel": {
"usage": "用法setworldlevel <等级>", "usage": "用法setworldlevel <等级>",
"value_error": "世界等级必须设置在0-8之间。", "value_error": "世界等级必须设置在0-8之间。",
"success": "已将世界等级设为 %s。", "success": "已将世界等级设为 %s。",
"invalid_world_level": "无效的世界等级。", "invalid_world_level": "无效的世界等级。",
"description": "设置世界等级,执行命令后需重新登录以生效" "description": "设置世界等级,执行命令后需重新登录以生效"
}, },
"spawn": { "spawn": {
"usage": "用法spawn <实体ID> [数量] [等级(仅怪物)]", "usage": "用法spawn <实体ID> [数量] [等级(仅怪物)]",
"success": "已生成 %s 个 %s。", "success": "已生成 %s 个 %s。",
"description": "在你附近生成一个生物" "description": "在你附近生成一个生物"
}, },
"stop": { "stop": {
"success": "正在关闭服务器...", "success": "正在关闭服务器...",
"description": "停止服务器" "description": "停止服务器"
}, },
"talent": { "talent": {
"usage_1": "设置天赋等级:/talent set <天赋ID> <数值>", "usage_1": "设置天赋等级:/talent set <天赋ID> <数值>",
"usage_2": "另一种设置天赋等级的方法:/talent <n (普攻) | e (元素战技) | q (元素爆发)> <数值>", "usage_2": "另一种设置天赋等级的方法:/talent <n (普) | e (元素战技) | q (元素爆发)> <数值>",
"usage_3": "获取天赋ID/talent getid", "usage_3": "获取天赋ID/talent getid",
"lower_16": "无效的天赋等级天赋等级应小于等于15。", "lower_16": "无效的天赋等级天赋等级应小于等于15。",
"set_id": "将天赋等级设为 %s。", "set_id": "将天赋等级设为 %s。",
@ -303,20 +304,20 @@
"normal_attack_id": "普通攻击的 ID 为 %s。", "normal_attack_id": "普通攻击的 ID 为 %s。",
"e_skill_id": "元素战技ID %s。", "e_skill_id": "元素战技ID %s。",
"q_skill_id": "元素爆发ID %s。", "q_skill_id": "元素爆发ID %s。",
"description": "设置当前角色的天赋等级" "description": "设置当前角色的天赋等级"
}, },
"teleportAll": { "teleportAll": {
"success": "已将所有玩家传送到你的位置", "success": "已将所有玩家传送到你的位置",
"error": "你只能在多人游戏状态下执行此命令。", "error": "你只能在多人游戏状态下执行此命令。",
"description": "将你世界中的所有玩家传送到你所在的位置" "description": "将你世界中的所有玩家传送到你所在的位置"
}, },
"teleport": { "teleport": {
"usage_server": "用法:/tp @<玩家ID> <x> <y> <z> [场景ID]", "usage_server": "用法:/tp @<玩家ID> <x> <y> <z> [场景ID]",
"usage": "用法:/tp [@<玩家ID>] <x> <y> <z> [场景ID]", "usage": "用法:/tp [@<玩家ID>] <x> <y> <z> [场景ID]",
"specify_player_id": "你必须指定一个玩家ID。", "specify_player_id": "你必须指定一个玩家ID。",
"invalid_position": "无效的位置。", "invalid_position": "无效的位置。",
"success": "传送 %s 到坐标 %s,%s,%s场景为 %s", "success": "传送 %s 到坐标 %s,%s,%s场景为 %s",
"description": "改变指定玩家的位置" "description": "改变指定玩家的位置"
}, },
"tower": { "tower": {
"unlock_done": "深境回廊的所有层已全部解锁。" "unlock_done": "深境回廊的所有层已全部解锁。"
@ -325,28 +326,37 @@
"usage": "用法weather <天气ID> [气候ID]", "usage": "用法weather <天气ID> [气候ID]",
"success": "已更改天气为 %s气候为 %s。", "success": "已更改天气为 %s气候为 %s。",
"invalid_id": "无效的天气ID。", "invalid_id": "无效的天气ID。",
"description": "更改天气" "description": "更改天气"
}, },
"drop": { "drop": {
"command_usage": "用法drop <物品ID|物品名称> [数量]", "command_usage": "用法drop <物品ID|物品名称> [数量]",
"success": "已丢下 %s 个 %s。", "success": "已丢下 %s 个 %s。",
"description": "在你附近丢下一个物品" "description": "在你附近丢下一个物品"
}, },
"help": { "help": {
"usage": "用法:", "usage": "用法:",
"aliases": "别名:", "aliases": "别名:",
"available_commands": "可用命令:", "available_commands": "可用命令:",
"description": "发送帮助信息或显示指定命令的信息" "description": "发送帮助信息或显示指定命令的信息"
}, },
"restart": { "restart": {
"description": "重新启动服务器" "description": "重新启动服务器"
}, },
"unlocktower": { "unlocktower": {
"success": "解锁完成。", "success": "解锁完成。",
"description": "解锁深境螺旋的所有层" "description": "解锁深境螺旋的所有层"
}, },
"resetshop": { "resetshop": {
"description": "重置商店刷新时间。" "description": "重置商店刷新时间"
}
},
"gacha": {
"details": {
"title": "Banner Details",
"available_five_stars": "Available 5-star Items",
"available_four_stars": "Available 4-star Items",
"available_three_stars": "Available 3-star Items",
"template_missing": "data/gacha_details.html is missing."
} }
} }
} }

View File

@ -45,7 +45,8 @@
"run_mode_error": "無效的伺服器運行模式: %s。", "run_mode_error": "無效的伺服器運行模式: %s。",
"run_mode_help": "伺服器運行模式必須為 HYBRID 或者 DISPATCH_ONLY 或者 GAME_ONLY。Grasscutter 啟動失敗...", "run_mode_help": "伺服器運行模式必須為 HYBRID 或者 DISPATCH_ONLY 或者 GAME_ONLY。Grasscutter 啟動失敗...",
"create_resources": "正在建立 resources 資料夾...", "create_resources": "正在建立 resources 資料夾...",
"resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。" "resources_error": "請將 BinOutput 和 ExcelBinOutput 複製到 resources 資料夾。",
"version": "Grasscutter版本: %s-%s"
} }
}, },
"commands": { "commands": {
@ -301,5 +302,14 @@
"resetshop": { "resetshop": {
"description": "重置商店時間" "description": "重置商店時間"
} }
},
"gacha": {
"details": {
"title": "Banner Details",
"available_five_stars": "Available 5-star Items",
"available_four_stars": "Available 4-star Items",
"available_three_stars": "Available 3-star Items",
"template_missing": "data/gacha_details.html is missing."
}
} }
} }