mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-21 18:58:19 +00:00
Merge development
into plugin-auth
This commit is contained in:
commit
a2ff8c8470
2
.gitignore
vendored
2
.gitignore
vendored
@ -69,6 +69,8 @@ language/
|
||||
languages/
|
||||
gacha-mapping.js
|
||||
data/gacha_mappings.js
|
||||
BuildConfig.java
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
data/hk4e/announcement/
|
||||
|
@ -139,6 +139,7 @@ There is a dummy user named "Server" in every player's friends list that you can
|
||||
| talent | talent \<talentID> \<value> | player.settalent | Client only | Sets talent level for your currently selected character | |
|
||||
| teleport | teleport [@playerUid] \<x> \<y> \<z> [sceneId] | player.teleport | Both side | Change the player's position. | tp |
|
||||
| tpall | | player.tpall | Client only | Teleports all players in your world to your position | |
|
||||
| unlocktower | | player.tower | Client only | Unlock the all floors of abyss | ut |
|
||||
| weather | weather \<weatherID> \<climateID> | player.weather | Client only | Changes the weather | w |
|
||||
|
||||
### Bonus
|
||||
|
@ -140,6 +140,7 @@ chmod +x gradlew
|
||||
| talent | talent <天赋ID> <等级> | player.settalent | 仅客户端 | 设置当前角色的天赋等级 | |
|
||||
| teleport | teleport [@playerUid] \<x> \<y> \<z> [sceneId] | player.teleport | 均可使用 | 传送玩家到指定坐标 | tp |
|
||||
| tpall | | player.tpall | 仅客户端 | 传送多人世界中所有的玩家到自身地点 | |
|
||||
| unlocktower | | player.tower | 仅客户端 | 解锁深渊全部层 | ut |
|
||||
| weather | weather <天气ID> <气候ID> | player.weather | 仅客户端 | 改变天气 | w |
|
||||
|
||||
### 额外功能
|
||||
|
20
build.gradle
20
build.gradle
@ -45,6 +45,7 @@ targetCompatibility = JavaVersion.VERSION_17
|
||||
group = 'xyz.grasscutters'
|
||||
version = '1.1.2-dev'
|
||||
|
||||
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
|
||||
@ -100,12 +101,14 @@ application {
|
||||
mainClassName = 'emu.grasscutter.Grasscutter'
|
||||
}
|
||||
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Main-Class': 'emu.grasscutter.Grasscutter'
|
||||
}
|
||||
|
||||
jar.baseName = 'grasscutter'
|
||||
jar.archiveName = project.hasProperty('jarFilename') ? "${jarFilename}.${extension}" : archiveName
|
||||
|
||||
from {
|
||||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||
@ -229,6 +232,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 {
|
||||
dependsOn "generateProto"
|
||||
}
|
||||
|
@ -6,12 +6,15 @@
|
||||
"prefabPath": "GachaShowPanel_A022",
|
||||
"previewPrefabPath": "UI_Tab_GachaShowPanel_A022",
|
||||
"titlePath": "UI_GACHA_SHOW_PANEL_A022_TITLE",
|
||||
"costItem": 224,
|
||||
"costItemId": 224,
|
||||
"costItemAmount": 1,
|
||||
"costItemAmount10": 10,
|
||||
"beginTime": 0,
|
||||
"endTime": 1924992000,
|
||||
"sortId": 1000,
|
||||
"rateUpItems1": [],
|
||||
"rateUpItems2": []
|
||||
"fallbackItems4Pool1": [1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064],
|
||||
"weights4": [[1,510], [8,510], [10,10000]],
|
||||
"weights5": [[1,75], [73,150], [90,10000]]
|
||||
},
|
||||
{
|
||||
"gachaType": 301,
|
||||
@ -20,13 +23,14 @@
|
||||
"prefabPath": "GachaShowPanel_A079",
|
||||
"previewPrefabPath": "UI_Tab_GachaShowPanel_A079",
|
||||
"titlePath": "UI_GACHA_SHOW_PANEL_A048_TITLE",
|
||||
"costItem": 223,
|
||||
"costItemId": 223,
|
||||
"beginTime": 0,
|
||||
"endTime": 1924992000,
|
||||
"sortId": 9998,
|
||||
"maxItemType": 1,
|
||||
"rateUpItems1": [1002],
|
||||
"rateUpItems2": [1053, 1020, 1045]
|
||||
"rateUpItems4": [1053, 1020, 1045],
|
||||
"rateUpItems5": [1002],
|
||||
"fallbackItems5Pool2": [],
|
||||
"weights5": [[1,80], [73,80], [90,10000]]
|
||||
},
|
||||
{
|
||||
"gachaType": 302,
|
||||
@ -35,15 +39,17 @@
|
||||
"prefabPath": "GachaShowPanel_A080",
|
||||
"previewPrefabPath": "UI_Tab_GachaShowPanel_A080",
|
||||
"titlePath": "UI_GACHA_SHOW_PANEL_A021_TITLE",
|
||||
"costItem": 223,
|
||||
"costItemId": 223,
|
||||
"beginTime": 0,
|
||||
"endTime": 1924992000,
|
||||
"sortId": 9997,
|
||||
"minItemType": 2,
|
||||
"eventChance": 75,
|
||||
"softPity": 80,
|
||||
"hardPity": 80,
|
||||
"rateUpItems1": [11509, 12504],
|
||||
"rateUpItems2": [11401, 12402, 13407, 14401, 15401]
|
||||
"rateUpItems4": [11401, 12402, 13407, 14401, 15401],
|
||||
"rateUpItems5": [11509, 12504],
|
||||
"fallbackItems5Pool1": [],
|
||||
"weights4": [[1,600], [7,600], [8, 6600], [10,12600]],
|
||||
"weights5": [[1,100], [62,100], [73, 7800], [80,10000]]
|
||||
}
|
||||
]
|
||||
|
@ -1,29 +1,22 @@
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"ann_id": 1,
|
||||
"title": "<b>Welcome to Grasscutter!</b>",
|
||||
"subtitle": "<b>Welcome</b>",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg",
|
||||
"content": "Hi there!<br>First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you! Check out our:<br><div><p style=\"white-space: pre-wrap;\"><strong>¡þDiscord¡þ</strong></p><p style=\"white-space: pre-wrap;\"><a href=\"https://discord.gg/T5vZU6UyeG\">https://discord.gg/T5vZU6UyeG</a></p><p style=\"white-space: pre-wrap;\"><strong>¡þGitHub¡þ</strong></p><p style=\"white-space: pre-wrap;\"><a href=\"https://github.com/Grasscutters/Grasscutter\">https://github.com/Grasscutters/Grasscutter</a></p></div>",
|
||||
"lang": "es-es"
|
||||
},
|
||||
{
|
||||
"ann_id": 2,
|
||||
"title": "<b>How to use announcements</b>",
|
||||
"subtitle": "<b>How to use</b>",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg",
|
||||
"content": "<strong>Tips<br></strong>>How to use announcements<br><br>>Announcement content can use HTML<br><br>>The specific content of the announcement is stored in the program directory<code>data/GameAnnouncement.json</code>, while<code>GameAnnouncementList.json</code> stores the announcement list data<br><br><strong>How to use</strong><br>>In <code>GameAnnouncement</code><table><thead><thead><tr><th>Parameters</th><th>Description</th></thead></thead><thbody><thead><tr><th>ann_Id</th><th>Announcement unique id</th></thead><thead><tr><th>title</th><th>Show at the top of the content</th></thead><thead><tr><th>subtitle</th><th>title shown on the left</th></thead><thead><tr><th>banner</th><th>Display between content and title</th></thead><thead><tr><th>content</th><th>as u see</th></thead><thead><tr><th>lang</th><th>display language</th></thead><thead><tr><th>total</th><th>Announcement quantity</th></thead></thbody></table><br><br>>In <code>GameAnnouncementList</code><br>If you want to add an annouement, please add the list data in the announcement type corresponding to GameAnnouncementList, and finally add the announcement content in GameAnnouncement",
|
||||
"lang": "es-es"
|
||||
},
|
||||
{
|
||||
"ann_id": 3,
|
||||
"title": "<b>ÕâÊǻ¹«¸æ--This is the event announcement</b>",
|
||||
"subtitle": "<b>Welcome</b>",
|
||||
"banner":"https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg",
|
||||
"content": "Welcome",
|
||||
"lang": "es-es"
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
"list": [
|
||||
{
|
||||
"ann_id": 1,
|
||||
"title": "<strong>Welcome to Grasscutter!</strong>",
|
||||
"subtitle": "Welcome!",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg",
|
||||
"content": "<p>Hi there!</p><p>First of all, welcome to Grasscutter. If you have any issues, please let us know so that Lawnmower can help you!</p><br><p><strong>〓Discord〓</strong></p><a href=\"https://discord.gg/T5vZU6UyeG\">https://discord.gg/T5vZU6UyeG</a><br><br><p><strong>〓GitHub〓</strong><a href=\"https://github.com/Grasscutters/Grasscutter\">https://github.com/Grasscutters/Grasscutter</a>",
|
||||
|
||||
"lang": "en-US"
|
||||
},
|
||||
{
|
||||
"ann_id": 2,
|
||||
"title": "<strong>How to use announcements</strong>",
|
||||
"subtitle": "How to use announcements",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/f4aa42d505822805eebf4a55d72a78d8_2755691727027973637.jpg",
|
||||
"content": "<p>Announcement content uses HTML. The specific content of the announcement is stored in the program directory <code>GameAnnouncement.json</code>, while <code>GameAnnouncementList.json</code> stores the announcement list data.</p><h2><code>GameAnnouncement</code></h2><table><tr><th>Parameter</th><th>Description</th></tr><tr><td>ann_id</td><td>Unique ID</td></tr><tr><td>title</td><td>Title shown at the top of the content</td></tr><tr><td>subtitle</td><td>Short title shown on the left</td></tr><tr><td>banner</td><td>Image to display between content and title</td></tr><tr><td>content</td><td>Content body in HTML</td></tr><tr><td>lang</td><td>Language code for this entry</td></tr></table><h2><code>GameAnnouncementList</code></h2><p>If you want to add an announcement, please add the list data in the announcement type corresponding to <code>GameAnnouncementList</code>, and finally add the announcement content in <code>GameAnnouncement</code>.</p>",
|
||||
"lang": "en-US"
|
||||
}
|
||||
],
|
||||
"total": 2
|
||||
}
|
@ -5,114 +5,64 @@
|
||||
"list": [
|
||||
{
|
||||
"ann_id": 1,
|
||||
"title": "<b>Welcome to Grasscutter!</b>",
|
||||
"subtitle": "<b>Welcome</b>",
|
||||
|
||||
"title": "<strong>Welcome to Grasscutter!</strong>",
|
||||
"subtitle": "Welcome!",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg",
|
||||
"content": "",
|
||||
"type_label": "Juego",
|
||||
"tag_label": "1",
|
||||
"tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png",
|
||||
"login_alert": 1,
|
||||
"lang": "es-es",
|
||||
"start_time": "2020-09-25 04:05:30",
|
||||
"end_time": "2023-10-30 11:00:00",
|
||||
"type": 2,
|
||||
"remind": 0,
|
||||
"alert": 0,
|
||||
"tag_start_time": "2000-01-02 15:04:05",
|
||||
"tag_end_time": "2030-01-02 15:04:05",
|
||||
"remind_ver": 1,
|
||||
"has_content": true,
|
||||
"extra_remind": 0
|
||||
"type_label": "System",
|
||||
"lang": "en-US",
|
||||
"start_time": "2020-09-25 04:05:30",
|
||||
"end_time": "2030-10-30 11:00:00",
|
||||
"content": "",
|
||||
"has_content": true
|
||||
},
|
||||
{
|
||||
"ann_id": 2,
|
||||
"title": "<b>这是游戏公告 -- This is the game announcement</b>",
|
||||
"subtitle": "<b>This is the game announcement</b>",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/17/85b7163c95745a76d49b3d163d893592_6487108933004985049.jpg",
|
||||
"content": "",
|
||||
"type_label": "Juego",
|
||||
"tag_label": "1",
|
||||
"title": "<strong>How to use announcements</strong>",
|
||||
"subtitle": "How to use announcements",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg",
|
||||
"tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png",
|
||||
"login_alert": 1,
|
||||
"lang": "es-es",
|
||||
"start_time": "2020-09-25 15:12:09",
|
||||
"end_time": "2030-10-30 11:00:00",
|
||||
"type": 2,
|
||||
"remind": 0,
|
||||
"alert": 0,
|
||||
"tag_start_time": "2000-01-02 08:04:05",
|
||||
"tag_end_time": "2030-01-02 08:04:05",
|
||||
"remind_ver": 1,
|
||||
"has_content": true,
|
||||
"extra_remind": 0
|
||||
"type_label": "System",
|
||||
"lang": "en-US",
|
||||
"start_time": "2020-09-25 04:05:30",
|
||||
"end_time": "2030-10-30 11:00:00",
|
||||
"content": "",
|
||||
"has_content": true
|
||||
}
|
||||
],
|
||||
"type_id": 2,
|
||||
"type_label": "Juego"
|
||||
},
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"ann_id": 3,
|
||||
"title": "<b>这是活动公告--This is the event announcement</b>",
|
||||
"subtitle": "<b>Welcome</b>",
|
||||
"banner": "https://uploadstatic-sea.mihoyo.com/announcement/2020/09/22/7d85f19b152d218e73224d7c138a0fd0_5818585260283672899.jpg",
|
||||
"content": "",
|
||||
"type_label": "Eventos",
|
||||
"tag_label": "1",
|
||||
"tag_icon": "https://uploadstatic-sea.mihoyo.com/announcement/2020/03/05/a2588f1a51faee9fa8dfe9aead649dd6_7237021399135895303.png",
|
||||
"login_alert": 1,
|
||||
"lang": "es-es",
|
||||
"start_time": "2020-09-25 04:05:30",
|
||||
"end_time": "2022-05-02 00:51:00",
|
||||
"type": 2,
|
||||
"remind": 0,
|
||||
"alert": 0,
|
||||
"tag_start_time": "2000-01-02 15:04:05",
|
||||
"tag_end_time": "2022-05-02 00:51:00",
|
||||
"remind_ver": 1,
|
||||
"has_content": true,
|
||||
"extra_remind": 0
|
||||
}
|
||||
],
|
||||
"type_id": 1,
|
||||
"type_label": "Eventos"
|
||||
"type_label": "System"
|
||||
},
|
||||
{
|
||||
"list": [
|
||||
{}
|
||||
],
|
||||
"type_id": 3,
|
||||
"type_label": "Others"
|
||||
"type_label": "Events"
|
||||
}
|
||||
],
|
||||
"total": 3,
|
||||
"total": 2,
|
||||
"type_list": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "游戏系统公告",
|
||||
"mi18n_name": "Juego"
|
||||
"name": "游戏系统公告",
|
||||
"mi18n_name": "System"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "活动公告",
|
||||
"mi18n_name": "Eventos"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "其他",
|
||||
"mi18n_name": "Others"
|
||||
"name": "活动公告",
|
||||
"mi18n_name": "Activity"
|
||||
}
|
||||
],
|
||||
"alert": true,
|
||||
"alert_id": 2,
|
||||
"timezone": -5,
|
||||
"pic_list": [
|
||||
],
|
||||
"alert": false,
|
||||
"alert_id": 0,
|
||||
"pic_list": [],
|
||||
"pic_total": 0,
|
||||
"pic_type_list": [
|
||||
],
|
||||
"pic_type_list": [],
|
||||
"pic_alert": false,
|
||||
"pic_alert_id": 0,
|
||||
"static_sign": ""
|
||||
|
121
data/gacha_details.html
Normal file
121
data/gacha_details.html
Normal 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>
|
@ -53,47 +53,14 @@
|
||||
}
|
||||
</style>
|
||||
<title>Gacha Records</title>
|
||||
<script>
|
||||
// Debug entry
|
||||
// record = [
|
||||
// {"time": 10000341, "item": 1041},
|
||||
// {"time": 10000342, "item": 1032},
|
||||
// {"time": 10000343, "item": 1035},
|
||||
// ];
|
||||
// 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", "blue"],
|
||||
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",
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<!-- This file could be generated automatically using `java -jar grasscutter.jar -gachamap` -->
|
||||
<!-- You can also modify the file manually to customize it -->
|
||||
<!-- Otherwise you may onle see number IDs in the gacha record -->
|
||||
<script type="text/javascript" src="/gacha/mappings"></script>
|
||||
<script>
|
||||
record = {{REPLACE_RECORD}};
|
||||
maxPage = {{REPLACE_MAXPAGE}};
|
||||
|
||||
mappings['default'] = mappings['en-us']; // make en-us as default/fallback option
|
||||
</script>
|
||||
</head>
|
||||
@ -161,32 +128,12 @@
|
||||
}
|
||||
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")+
|
||||
"."+String(date.getMilliseconds()).padStart(3, "0");
|
||||
} 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")+
|
||||
"."+String(date.getMilliseconds()).padStart(3, "0");
|
||||
}
|
||||
}
|
||||
(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>";
|
||||
e.innerHTML= "<td>" + (new Date(element.time).toLocaleString(lang)) + "</td><td>" + itemMapper(element.item) + "</td>";
|
||||
container.appendChild(e);
|
||||
});
|
||||
// setup pagenation buttons
|
||||
|
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
ElIKBm9zX3VzYRIHQW1lcmljYRoKREVWX1BVQkxJQyIzaHR0cHM6Ly9vc3VzYWRpc3BhdGNoLnl1YW5zaGVuLmNvbS9xdWVyeV9jdXJfcmVnaW9uElMKB29zX2V1cm8SBkV1cm9wZRoKREVWX1BVQkxJQyI0aHR0cHM6Ly9vc2V1cm9kaXNwYXRjaC55dWFuc2hlbi5jb20vcXVlcnlfY3VyX3JlZ2lvbhJRCgdvc19hc2lhEgRBc2lhGgpERVZfUFVCTElDIjRodHRwczovL29zYXNpYWRpc3BhdGNoLnl1YW5zaGVuLmNvbS9xdWVyeV9jdXJfcmVnaW9uElUKBm9zX2NodBIKVFcsIEhLLCBNTxoKREVWX1BVQkxJQyIzaHR0cHM6Ly9vc2NodGRpc3BhdGNoLnl1YW5zaGVuLmNvbS9xdWVyeV9jdXJfcmVnaW9uKpwQRWMyYhAAAABbrAvbhfIRHfaSCN24qQyVAAgAAMs68ZiMdPfEj41O2wBCYqGiC/WdovvJvaw4t3/m1zIYDrt3/ftK9GKFb7C+2E8FmaHqOnwjJYBg2wI1sXpGmuSxkeWw8Avr36wlNtQjhXNV9zoNKstuZYuheyLlpbPRbYZ3UA6/BzTVsjIhjR1lcqFrigQnpV6MgRR9KqxakCaffK6qIzMlodx4ZPKlqseQhCiyVAvLWQSRqCRcZipzotXsmgLQbpDFtRzhgukXPjfW5dAlzMwswPuu7ZQsf1AKipI34dVQLu6gtXthGgbjn89h/79VR5AokLCPGqIV7/2s+gHfykrjDtyp5rwCcmGQqwV3gHy5LGrHl8Zm12jNd7Qcng51ydqtX4xzet6J2iMF6Dw5nPd/hTyxn+i3Ttk6fop9rbCq3iNgEw3+0cSDal1I1ThYdVnMgPhZgQkZc5/SpTaR+8vfDzRIKbSSrrPSEgLnQvWZOOugXhNdyuiaBc8rJveno7vvktmnhDUF3xWi6osj75j2KghRrdHfDR3Zuh4COrGZDRBSKHft2AvfrxaMT9O8hPzzzYk0U2iicVCDlNP/8wqaT9Vqt1kHmruLxqh377iyp0mxKfNt0+SNRzLyRoyvOar/z3AT6TU9LRoCFrkcJpVsUN+2MVeT52PfMbv5O/Nw9sqsFDlofCJJ/EknY0wDc+tNarYOhDM67/ojn/p6W3ZPBJxb2wcF1TOh9dpAeZdCGJusqhMIj5lpoW8nENTFhkEgMUv2Lh5Z6WpeOAKAu9eDpBMhlRNCccDaNYUgo6TdVDtWxtPrS3NRYqtkvb2I2SEFP0apht954oKdG3ncxyOgHRUkwgtxbCMAngzWo9+VWV3H3OlqeEOv7DdO2o0y95EvlHYb/qtosXPI2jC+6FPa+yl4xmLqcENRTUrU23dsmX3SyBEmZvML4dNeyC53B+mh7DUFtPFJFndxj2tGO9mTSDgy8eCmKG90AiJOMoxaLB2HpnDXN1sTiIcd3WraiE6ZCt4E54hKXvXHPyN52CHkxq1y/TeXHEq4X4MyHyDSRLHmzVs9pnwHM0ZLthKFNyvGfTvjiYokAWtNEuh74syt+m6Wietb6JvgibnnDj6uFKI3BbH4GUT9blsnMgug323bJ6bFvV4iESvz1fNnnUSokWQy5+fWzxPDohULgFzhDCpwov78Bp0E3t6DXSWnrUdNqpLbYKmXO1Hdbn+QH4B90p85UB1V5eSZgxPpUvZbIO4GPScil8K+dkDLdsFa1zypWNmlUN0Ns5H/iuzMuJql2QFYz+SnV1R1T+qywwqCNP9oswcLiAR3XnSacs52vd3PI9+0PZuoF6tVMWlvutsQ34IFZaAwIkdKigZcHumLBt/0KyFASBfN674n8FnHrHOQHU6oCeXkQA9kC8MtkvMb7fOLdzbTsD6SVojzZ64i9mDXxF+iLR9o52OxjIFzwLGRy/ivT/aAnHLZ3AsbnvslDjlQl2ADBFvf7xjmvFu0xlfK58TUpfVEkScFFapWJyKVybB4CRz1wKKz6n/a9581LpCVOWRsJa5p+j0zYcS2PfhmRf3RzwsDHeBjEVlIARbhxNKvmjdZyIidSdMMcsJHDRLE3bvo9kKfag0vRVKmuPLPc9FrACsz3vlkApcVQvzieHWoiP+foEvfj9+7Ti2tLfKdzVkMUmugZiZ46+7PKvIciiiuBPlyld0CCPTtTFHUOMO5dUfrUblX8K3awWiaNQFBS0J3iK08t1bgWfLhsKzsS32fRWugaqecwO9Rji9oHn+UuN8Nz9SgNxodroq9q7y/KHFxbqjCl62g25HN9zUa/s5wnIRwVAiWgTuOe3qGqjwp5m/GR8YVSSK/8mV9EL4AaF8d1uifdVA6wWSH1e/1UB8vcdU83P8ne3u1ho+Y/57WB7KnQaGaiD/108+wiAxNqMb2ex8on01VxdLKV1makXV3gzsvWaRevW8t/K11ZwYfo9g+guWADsA0JO0jWooiaupq1kNWrEheBdSRXBO7Jnb+56cTjPGwLpp7ZOHe/bSCJ4MGzPF3lK66LXhVo+rxvNjhoKVRjhGYxN4T8+AiRo3r+1KwdIGSrtODp3ri3JWAy6Eajp1Ukp9GaCbHSJFnYml84nKew7zLLe//ExQpjd4QAjMTvnbm+Ff6a1jf69QEVo0I33gI7/buwqgjiuvjeL6EYaMolKrKlHZHf/HwWbFbdID8T9aoyZJuCUd6YHaMPRAS6n5nvTwkRLlJ/f6wgyypUGZ22Bb1qGIb9SoPgSgIJkifUoewQW2EexqfoAsHXJVABLy+jp/SC4xzHZOSh42zU1k80HIgrnSOmu6T56F6gqy4Y2cZuZU8LXbO/01u8ifEz8yaXfEFSFdxE0TWl92OLKFtJZr9nNOBQQQr5FDGf6zB1/0CziG/5+PrUDgG3irzho6+7wXkc2CpxlBKOLWdjs3V/Lab6cURz1QZY4HYgUkJtm4U5OKUeO2+murlhC7SrnwyUtGrsD8NbCmI4SRHKPoeLBJQO/m3dRze5Ltr8N9IS7/ukPeOYe1O2agrmhH/JjYfz/l8Gmq8PGY+oavYp8I+2yKvGLD9kCxEgKcTeRh9AW/xPTLGsacrGKQCY+M76DfyLKxCZDiDY9xkBIKchxsMsn7FqZvRMMyJBHbqa3AKQyAN73NCSuFF5f1qDjARU/xqJFhOaKoR64c78oqh1GqOqEFbfNQIRw6WeFCGyW6v6p10uLdR7KXnR7+wub9aG992MpIBk0+gru74yO/WcA0vLdDEQIBwc+M0lmLB53ylsPtde3nliaC5ROHR1IS4LO8Q+3o0BHMr0my0bqFwwCAvZVXOFBHxXyUgrrmUTnZYVSQXNV6+MALBmmRU5yOzhhyHoEdj9YHZeyPpZkYc6DkJWCRYbFfmczNIs133KB9rlfug40w/hHa8pXyRyLaKQUMIUYEvt3Y4AQ==
|
@ -34,6 +34,7 @@ import emu.grasscutter.utils.Language;
|
||||
import emu.grasscutter.server.game.GameServer;
|
||||
import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.Crypto;
|
||||
import emu.grasscutter.BuildConfig;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@ -88,6 +89,9 @@ public final class Grasscutter {
|
||||
case "-gachamap" -> {
|
||||
Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true;
|
||||
}
|
||||
case "-version" -> {
|
||||
System.out.println("Grasscutter version: " + BuildConfig.VERSION + "-" + BuildConfig.GIT_HASH); exitEarly = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +130,9 @@ public final class Grasscutter {
|
||||
httpServer.addRouter(DispatchHandler.class);
|
||||
httpServer.addRouter(LegacyAuthHandler.class);
|
||||
httpServer.addRouter(GachaHandler.class);
|
||||
|
||||
// TODO: find a better place?
|
||||
StaminaManager.initialize();
|
||||
|
||||
// Start servers.
|
||||
var runMode = SERVER.runMode;
|
||||
@ -178,16 +185,21 @@ public final class Grasscutter {
|
||||
* Attempts to load the configuration from a file.
|
||||
*/
|
||||
public static void loadConfig() {
|
||||
// Check if config.json exists. If not, we generate a new config.
|
||||
if (!configFile.exists()) {
|
||||
getLogger().info("config.json could not be found. Generating a default configuration ...");
|
||||
config = new ConfigContainer();
|
||||
Grasscutter.saveConfig(config);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the file already exists, we attempt to load it.
|
||||
try (FileReader file = new FileReader(configFile)) {
|
||||
config = gson.fromJson(file, ConfigContainer.class);
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.saveConfig(null);
|
||||
config = new ConfigContainer();
|
||||
} catch (Error error) {
|
||||
// Occurred probably from an outdated config file.
|
||||
Grasscutter.saveConfig(null);
|
||||
config = new ConfigContainer();
|
||||
}
|
||||
getLogger().error("There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json.");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,13 @@ public interface AuthenticationSystem {
|
||||
*/
|
||||
void resetPassword(String username);
|
||||
|
||||
/**
|
||||
* Called by plugins to internally verify a user's identity.
|
||||
* @param details A unique, one-time token to verify the user.
|
||||
* @return True if the user is verified, False otherwise.
|
||||
*/
|
||||
boolean verifyUser(String details);
|
||||
|
||||
/**
|
||||
* This is the authenticator used for password authentication.
|
||||
* @return An authenticator.
|
||||
|
@ -1,9 +1,12 @@
|
||||
package emu.grasscutter.auth;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.auth.DefaultAuthenticators.*;
|
||||
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
||||
import emu.grasscutter.server.http.objects.LoginResultJson;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
/**
|
||||
* The default Grasscutter authentication implementation.
|
||||
* Allows all users to access any account.
|
||||
@ -22,6 +25,12 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
||||
public void resetPassword(String username) {
|
||||
// Unhandled. The default authenticator doesn't store passwords.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyUser(String details) {
|
||||
Grasscutter.getLogger().info(translate("dispatch.authentication.default_unable_to_verify"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator<LoginResultJson> getPasswordAuthenticator() {
|
||||
|
@ -0,0 +1,46 @@
|
||||
package emu.grasscutter.command.commands;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.command.Command;
|
||||
import emu.grasscutter.command.CommandHandler;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.server.packet.send.PacketChangeMpTeamAvatarRsp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
@Command(label = "join", usage = "join [AvatarIDs] such as\"join 10000038 10000039\"",
|
||||
description = "commands.join.description", permission = "player.join")
|
||||
public class JoinCommand implements CommandHandler {
|
||||
|
||||
@Override
|
||||
public void execute(Player sender, Player targetPlayer, List<String> args) {
|
||||
List<Integer> avatarIds = new ArrayList<>();
|
||||
for (String arg : args) {
|
||||
try {
|
||||
int avatarId = Integer.parseInt(arg);
|
||||
avatarIds.add(avatarId);
|
||||
} catch (Exception ignored) {
|
||||
ignored.printStackTrace();
|
||||
CommandHandler.sendMessage(sender, translate("commands.generic.invalid.avatarId"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
Avatar avatar = sender.getAvatars().getAvatarById(avatarIds.get(i));
|
||||
if (avatar == null || sender.getTeamManager().getCurrentTeamInfo().contains(avatar)) {
|
||||
CommandHandler.sendMessage(sender, translate("commands.generic.invalid.avatarId"));
|
||||
return;
|
||||
}
|
||||
sender.getTeamManager().getCurrentTeamInfo().addAvatar(avatar);
|
||||
}
|
||||
|
||||
// Packet
|
||||
sender.getTeamManager().updateTeamEntities(new PacketChangeMpTeamAvatarRsp(sender, sender.getTeamManager().getCurrentTeamInfo()));
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package emu.grasscutter.command.commands;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.command.Command;
|
||||
import emu.grasscutter.command.CommandHandler;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
@Command(label = "quest", usage = "quest <add|finish> [quest id]", permission = "player.quest", permissionTargeted = "player.quest.others", description = "commands.quest.description")
|
||||
public final class QuestCommand implements CommandHandler {
|
||||
|
||||
@Override
|
||||
public void execute(Player sender, Player targetPlayer, List<String> args) {
|
||||
if (targetPlayer == null) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() != 2) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.usage"));
|
||||
return;
|
||||
}
|
||||
|
||||
String cmd = args.get(0).toLowerCase();
|
||||
int questId;
|
||||
|
||||
try {
|
||||
questId = Integer.parseInt(args.get(1));
|
||||
} catch (Exception e) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.invalid_id"));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case "add" -> {
|
||||
GameQuest quest = targetPlayer.getQuestManager().addQuest(questId);
|
||||
|
||||
if (quest != null) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.added", questId));
|
||||
return;
|
||||
}
|
||||
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found"));
|
||||
}
|
||||
case "finish" -> {
|
||||
GameQuest quest = targetPlayer.getQuestManager().getQuestById(questId);
|
||||
|
||||
if (quest == null) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
quest.finish();
|
||||
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.finished", questId));
|
||||
}
|
||||
default -> {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.usage"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,6 @@ public final class ReloadCommand implements CommandHandler {
|
||||
Grasscutter.getGameServer().getGachaManager().load();
|
||||
Grasscutter.getGameServer().getDropManager().load();
|
||||
Grasscutter.getGameServer().getShopManager().load();
|
||||
// Grasscutter.getHttpServer().loadQueries(); // Is this practical?
|
||||
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done"));
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
package emu.grasscutter.command.commands;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.command.Command;
|
||||
import emu.grasscutter.command.CommandHandler;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.server.packet.send.PacketChangeMpTeamAvatarRsp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
@Command(label = "remove", usage = "remove [indexOfYourTeams] index start from 1",
|
||||
description = "commands.remove.description", permission = "player.remove")
|
||||
public class RemoveCommand implements CommandHandler {
|
||||
|
||||
@Override
|
||||
public void execute(Player sender, Player targetPlayer, List<String> args) {
|
||||
List<Integer> avatarIds = new ArrayList<>();
|
||||
for (String arg : args) {
|
||||
try {
|
||||
int avatarId = Integer.parseInt(arg);
|
||||
avatarIds.add(avatarId);
|
||||
} catch (Exception ignored) {
|
||||
ignored.printStackTrace();
|
||||
CommandHandler.sendMessage(sender, translate("commands.remove.invalid_index"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < avatarIds.size(); i++) {
|
||||
if (avatarIds.get(i) > sender.getTeamManager().getCurrentTeamInfo().getAvatars().size() || avatarIds.get(i) <= 0) {
|
||||
CommandHandler.sendMessage(sender, translate("commands.remove.invalid_index"));
|
||||
return;
|
||||
}
|
||||
sender.getTeamManager().getCurrentTeamInfo().removeAvatar(avatarIds.get(i) - 1);
|
||||
}
|
||||
|
||||
// Packet
|
||||
sender.getTeamManager().updateTeamEntities(new PacketChangeMpTeamAvatarRsp(sender, sender.getTeamManager().getCurrentTeamInfo()));
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ import java.util.Map;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import emu.grasscutter.data.custom.AbilityEmbryoEntry;
|
||||
import emu.grasscutter.data.custom.AbilityModifier;
|
||||
import emu.grasscutter.data.custom.AbilityModifierEntry;
|
||||
import emu.grasscutter.data.custom.OpenConfigEntry;
|
||||
import emu.grasscutter.data.custom.MainQuestData;
|
||||
import emu.grasscutter.data.custom.ScenePointEntry;
|
||||
import emu.grasscutter.data.def.*;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap;
|
||||
@ -27,6 +27,7 @@ public class GameData {
|
||||
private static final Map<String, AbilityModifierEntry> abilityModifiers = new HashMap<>();
|
||||
private static final Map<String, OpenConfigEntry> openConfigEntries = new HashMap<>();
|
||||
private static final Map<String, ScenePointEntry> scenePointEntries = new HashMap<>();
|
||||
private static final Int2ObjectMap<MainQuestData> mainQuestData = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
// ExcelConfigs
|
||||
private static final Int2ObjectMap<PlayerLevelData> playerLevelDataMap = new Int2ObjectOpenHashMap<>();
|
||||
@ -63,11 +64,14 @@ public class GameData {
|
||||
|
||||
private static final Int2ObjectMap<SceneData> sceneDataMap = new Int2ObjectLinkedOpenHashMap<>();
|
||||
private static final Int2ObjectMap<FetterData> fetterDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<CodexQuest> codexQuestMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<CodexQuest> codexQuestIdMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<FetterCharacterCardData> fetterCharacterCardDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<RewardData> rewardDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<WorldLevelData> worldLevelDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<DailyDungeonData> dailyDungeonDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<DungeonData> dungeonDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<QuestData> questDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<ShopGoodsData> shopGoodsDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<CombineData> combineDataMap = new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<RewardPreviewData> rewardPreviewDataMap = new Int2ObjectOpenHashMap<>();
|
||||
@ -122,6 +126,10 @@ public class GameData {
|
||||
return getScenePointEntries().get(sceneId + "_" + pointId);
|
||||
}
|
||||
|
||||
public static Int2ObjectMap<MainQuestData> getMainQuestDataMap() {
|
||||
return mainQuestData;
|
||||
}
|
||||
|
||||
public static Int2ObjectMap<AvatarData> getAvatarDataMap() {
|
||||
return avatarDataMap;
|
||||
}
|
||||
@ -286,6 +294,10 @@ public class GameData {
|
||||
return fetters;
|
||||
}
|
||||
|
||||
public static Int2ObjectMap<CodexQuest> getCodexQuestMap(){return codexQuestMap;}
|
||||
|
||||
public static Int2ObjectMap<CodexQuest> getCodexQuestIdMap(){return codexQuestIdMap;}
|
||||
|
||||
public static Int2ObjectMap<WorldLevelData> getWorldLevelDataMap() {
|
||||
return worldLevelDataMap;
|
||||
}
|
||||
@ -331,4 +343,8 @@ public class GameData {
|
||||
public static Int2ObjectMap<TowerScheduleData> getTowerScheduleDataMap(){
|
||||
return towerScheduleDataMap;
|
||||
}
|
||||
|
||||
public static Int2ObjectMap<QuestData> getQuestDataMap() {
|
||||
return questDataMap;
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction;
|
||||
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType;
|
||||
import emu.grasscutter.data.custom.AbilityModifierEntry;
|
||||
import emu.grasscutter.data.custom.OpenConfigEntry;
|
||||
import emu.grasscutter.data.custom.MainQuestData;
|
||||
import emu.grasscutter.data.custom.ScenePointEntry;
|
||||
import emu.grasscutter.game.world.SpawnDataEntry.*;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
@ -58,8 +59,9 @@ public class ResourceLoader {
|
||||
loadResources();
|
||||
// Process into depots
|
||||
GameDepot.load();
|
||||
// Load spawn data
|
||||
// Load spawn data and quests
|
||||
loadSpawnData();
|
||||
loadQuests();
|
||||
// Load scene points - must be done AFTER resources are loaded
|
||||
loadScenePoints();
|
||||
// Custom - TODO move this somewhere else
|
||||
@ -394,6 +396,29 @@ public class ResourceLoader {
|
||||
GameData.getOpenConfigEntries().put(entry.getName(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadQuests() {
|
||||
File folder = new File(RESOURCE("BinOutput/Quest/"));
|
||||
|
||||
if (!folder.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : folder.listFiles()) {
|
||||
MainQuestData mainQuest = null;
|
||||
|
||||
try (FileReader fileReader = new FileReader(file)) {
|
||||
mainQuest = Grasscutter.getGsonFactory().fromJson(fileReader, MainQuestData.class);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
|
||||
GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest);
|
||||
}
|
||||
|
||||
Grasscutter.getLogger().info("Loaded " + GameData.getMainQuestDataMap().size() + " MainQuestDatas.");
|
||||
}
|
||||
|
||||
// BinOutput configs
|
||||
|
||||
|
53
src/main/java/emu/grasscutter/data/custom/MainQuestData.java
Normal file
53
src/main/java/emu/grasscutter/data/custom/MainQuestData.java
Normal file
@ -0,0 +1,53 @@
|
||||
package emu.grasscutter.data.custom;
|
||||
|
||||
import emu.grasscutter.game.quest.enums.LogicType;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.enums.QuestType;
|
||||
|
||||
public class MainQuestData {
|
||||
private int id;
|
||||
private int series;
|
||||
private QuestType type;
|
||||
|
||||
private long titleTextMapHash;
|
||||
private int[] suggestTrackMainQuestList;
|
||||
private int[] rewardIdList;
|
||||
|
||||
private SubQuestData[] subQuests;
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public QuestType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public long getTitleTextMapHash() {
|
||||
return titleTextMapHash;
|
||||
}
|
||||
|
||||
public int[] getSuggestTrackMainQuestList() {
|
||||
return suggestTrackMainQuestList;
|
||||
}
|
||||
|
||||
public int[] getRewardIdList() {
|
||||
return rewardIdList;
|
||||
}
|
||||
|
||||
public SubQuestData[] getSubQuests() {
|
||||
return subQuests;
|
||||
}
|
||||
|
||||
public static class SubQuestData {
|
||||
private int subId;
|
||||
|
||||
public int getSubId() {
|
||||
return subId;
|
||||
}
|
||||
}
|
||||
}
|
42
src/main/java/emu/grasscutter/data/def/CodexQuest.java
Normal file
42
src/main/java/emu/grasscutter/data/def/CodexQuest.java
Normal file
@ -0,0 +1,42 @@
|
||||
package emu.grasscutter.data.def;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.GameResource;
|
||||
import emu.grasscutter.data.ResourceType;
|
||||
|
||||
@ResourceType(name = {"QuestCodexExcelConfigData.json"}, loadPriority = ResourceType.LoadPriority.HIGH)
|
||||
public class CodexQuest extends GameResource {
|
||||
private int Id;
|
||||
private int ParentQuestId;
|
||||
private int ChapterId;
|
||||
private int SortOrder;
|
||||
private boolean IsDisuse;
|
||||
|
||||
public int getParentQuestId() {
|
||||
return ParentQuestId;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return Id;
|
||||
}
|
||||
|
||||
public int getChapterId() {
|
||||
return ChapterId;
|
||||
}
|
||||
|
||||
public int getSortOrder() {
|
||||
return SortOrder;
|
||||
}
|
||||
|
||||
public boolean getIsDisuse() {
|
||||
return IsDisuse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
if(!this.getIsDisuse()) {
|
||||
GameData.getCodexQuestIdMap().put(this.getParentQuestId(), this);
|
||||
}
|
||||
}
|
||||
}
|
126
src/main/java/emu/grasscutter/data/def/QuestData.java
Normal file
126
src/main/java/emu/grasscutter/data/def/QuestData.java
Normal file
@ -0,0 +1,126 @@
|
||||
package emu.grasscutter.data.def;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import emu.grasscutter.data.GameResource;
|
||||
import emu.grasscutter.data.ResourceType;
|
||||
import emu.grasscutter.game.quest.enums.LogicType;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
|
||||
@ResourceType(name = "QuestExcelConfigData.json")
|
||||
public class QuestData extends GameResource {
|
||||
private int SubId;
|
||||
private int MainId;
|
||||
private int Order;
|
||||
private long DescTextMapHash;
|
||||
|
||||
private boolean FinishParent;
|
||||
private boolean IsRewind;
|
||||
|
||||
private LogicType AcceptCondComb;
|
||||
private QuestCondition[] acceptConditons;
|
||||
private LogicType FinishCondComb;
|
||||
private QuestCondition[] finishConditons;
|
||||
private LogicType FailCondComb;
|
||||
private QuestCondition[] failConditons;
|
||||
|
||||
private List<QuestParam> AcceptCond;
|
||||
private List<QuestParam> FinishCond;
|
||||
private List<QuestParam> FailCond;
|
||||
private List<QuestExecParam> BeginExec;
|
||||
private List<QuestExecParam> FinishExec;
|
||||
private List<QuestExecParam> FailExec;
|
||||
|
||||
public int getId() {
|
||||
return SubId;
|
||||
}
|
||||
|
||||
public int getMainId() {
|
||||
return MainId;
|
||||
}
|
||||
|
||||
public int getOrder() {
|
||||
return Order;
|
||||
}
|
||||
|
||||
public long getDescTextMapHash() {
|
||||
return DescTextMapHash;
|
||||
}
|
||||
|
||||
public boolean finishParent() {
|
||||
return FinishParent;
|
||||
}
|
||||
|
||||
public boolean isRewind() {
|
||||
return IsRewind;
|
||||
}
|
||||
|
||||
public LogicType getAcceptCondComb() {
|
||||
return AcceptCondComb;
|
||||
}
|
||||
|
||||
public QuestCondition[] getAcceptCond() {
|
||||
return acceptConditons;
|
||||
}
|
||||
|
||||
public LogicType getFinishCondComb() {
|
||||
return FinishCondComb;
|
||||
}
|
||||
|
||||
public QuestCondition[] getFinishCond() {
|
||||
return finishConditons;
|
||||
}
|
||||
|
||||
public LogicType getFailCondComb() {
|
||||
return FailCondComb;
|
||||
}
|
||||
|
||||
public QuestCondition[] getFailCond() {
|
||||
return failConditons;
|
||||
}
|
||||
|
||||
public void onLoad() {
|
||||
this.acceptConditons = AcceptCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new);
|
||||
AcceptCond = null;
|
||||
this.finishConditons = FinishCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new);
|
||||
FinishCond = null;
|
||||
this.failConditons = FailCond.stream().filter(p -> p.Type != null).map(QuestCondition::new).toArray(QuestCondition[]::new);
|
||||
FailCond = null;
|
||||
}
|
||||
|
||||
public class QuestParam {
|
||||
QuestTrigger Type;
|
||||
int[] Param;
|
||||
String count;
|
||||
}
|
||||
|
||||
public class QuestExecParam {
|
||||
QuestTrigger Type;
|
||||
String[] Param;
|
||||
String count;
|
||||
}
|
||||
|
||||
public static class QuestCondition {
|
||||
private QuestTrigger type;
|
||||
private int[] param;
|
||||
private String count;
|
||||
|
||||
public QuestCondition(QuestParam param) {
|
||||
this.type = param.Type;
|
||||
this.param = param.Param;
|
||||
}
|
||||
|
||||
public QuestTrigger getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int[] getParam() {
|
||||
return param;
|
||||
}
|
||||
|
||||
public String getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import emu.grasscutter.game.gacha.GachaRecord;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.mail.Mail;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
|
||||
import static com.mongodb.client.model.Filters.eq;
|
||||
|
||||
@ -111,6 +112,8 @@ public final class DatabaseHelper {
|
||||
DatabaseManager.getGameDatabase().getCollection("gachas").deleteMany(eq("ownerId", target.getPlayerUid()));
|
||||
// Delete GameItem.class data
|
||||
DatabaseManager.getGameDatabase().getCollection("items").deleteMany(eq("ownerId", target.getPlayerUid()));
|
||||
// Delete GameMainQuest.class data
|
||||
DatabaseManager.getGameDatabase().getCollection("quests").deleteMany(eq("ownerUid", target.getPlayerUid()));
|
||||
|
||||
// Delete friendships.
|
||||
// Here, we need to make sure to not only delete the deleted account's friendships,
|
||||
@ -260,4 +263,16 @@ public final class DatabaseHelper {
|
||||
DeleteResult result = DatabaseManager.getGameDatastore().delete(mail);
|
||||
return result.wasAcknowledged();
|
||||
}
|
||||
|
||||
public static List<GameMainQuest> getAllQuests(Player player) {
|
||||
return DatabaseManager.getGameDatastore().find(GameMainQuest.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList();
|
||||
}
|
||||
|
||||
public static void saveQuest(GameMainQuest quest) {
|
||||
DatabaseManager.getGameDatastore().save(quest);
|
||||
}
|
||||
|
||||
public static boolean deleteQuest(GameMainQuest quest) {
|
||||
return DatabaseManager.getGameDatastore().delete(quest).wasAcknowledged();
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ import emu.grasscutter.game.gacha.GachaRecord;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.mail.Mail;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
|
||||
import static emu.grasscutter.Configuration.*;
|
||||
|
||||
@ -27,7 +29,8 @@ 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, GachaRecord.class, Mail.class
|
||||
DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class,
|
||||
GachaRecord.class, Mail.class, GameMainQuest.class
|
||||
};
|
||||
|
||||
public static Datastore getGameDatastore() {
|
||||
|
@ -144,16 +144,17 @@ public class Account {
|
||||
}
|
||||
|
||||
public boolean hasPermission(String permission) {
|
||||
if (this.permissions.contains(permission) || this.permissions.contains("*")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.permissions.contains(permission)) return true;
|
||||
if(this.permissions.contains("*") && this.permissions.size() == 1) return true;
|
||||
|
||||
String[] permissionParts = permission.split("\\.");
|
||||
for (String p : this.permissions) {
|
||||
if (permissionMatchesWildcard(p, permissionParts)) {
|
||||
return true;
|
||||
}
|
||||
if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) return false;
|
||||
if (permissionMatchesWildcard(p, permissionParts)) return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
return this.permissions.contains("*");
|
||||
}
|
||||
|
||||
public boolean removePermission(String permission) {
|
||||
|
@ -7,6 +7,7 @@ import emu.grasscutter.data.custom.ScenePointEntry;
|
||||
import emu.grasscutter.data.def.DungeonData;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.SceneType;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.server.game.GameServer;
|
||||
@ -51,8 +52,9 @@ public class DungeonManager {
|
||||
int sceneId = data.getSceneId();
|
||||
player.getScene().setPrevScene(sceneId);
|
||||
|
||||
if(player.getWorld().transferPlayerToScene(player, sceneId, data)){
|
||||
if (player.getWorld().transferPlayerToScene(player, sceneId, data)) {
|
||||
player.getScene().addDungeonSettleObserver(basicDungeonSettleObserver);
|
||||
player.getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_ENTER_DUNGEON, data.getId());
|
||||
}
|
||||
|
||||
player.getScene().setPrevScenePoint(pointId);
|
||||
|
@ -2,27 +2,48 @@ package emu.grasscutter.game.gacha;
|
||||
|
||||
import emu.grasscutter.net.proto.GachaInfoOuterClass.GachaInfo;
|
||||
import emu.grasscutter.net.proto.GachaUpInfoOuterClass.GachaUpInfo;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
import static emu.grasscutter.Configuration.*;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
|
||||
public class GachaBanner {
|
||||
private int gachaType;
|
||||
private int scheduleId;
|
||||
private String prefabPath;
|
||||
private String previewPrefabPath;
|
||||
private String titlePath;
|
||||
private int costItem;
|
||||
private int costItemId = 0;
|
||||
private int costItemAmount = 1;
|
||||
private int costItemId10 = 0;
|
||||
private int costItemAmount10 = 10;
|
||||
private int beginTime;
|
||||
private int endTime;
|
||||
private int sortId;
|
||||
private int[] rateUpItems1;
|
||||
private int[] rateUpItems2;
|
||||
private int baseYellowWeight = 60; // Max 10000
|
||||
private int basePurpleWeight = 510; // Max 10000
|
||||
private int eventChance = 50; // Chance to win a featured event item
|
||||
private int softPity = 75;
|
||||
private int hardPity = 90;
|
||||
private int[] rateUpItems4 = {};
|
||||
private int[] rateUpItems5 = {};
|
||||
private int[] fallbackItems3 = {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304};
|
||||
private int[] fallbackItems4Pool1 = {1014, 1020, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064};
|
||||
private int[] fallbackItems4Pool2 = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
|
||||
private int[] fallbackItems5Pool1 = {1003, 1016, 1042, 1035, 1041};
|
||||
private int[] fallbackItems5Pool2 = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
|
||||
private boolean removeC6FromPool = false;
|
||||
private boolean autoStripRateUpFromFallback = true;
|
||||
private int[][] weights4 = {{1,510}, {8,510}, {10,10000}};
|
||||
private int[][] weights5 = {{1,75}, {73,150}, {90,10000}};
|
||||
private int[][] poolBalanceWeights4 = {{1,255}, {17,255}, {21,10455}};
|
||||
private int[][] poolBalanceWeights5 = {{1,30}, {147,150}, {181,10230}};
|
||||
private int eventChance4 = 50; // Chance to win a featured event item
|
||||
private int eventChance5 = 50; // Chance to win a featured event item
|
||||
private BannerType bannerType = BannerType.STANDARD;
|
||||
|
||||
// Kinda wanna deprecate these but they're in people's configs
|
||||
private int[] rateUpItems1 = {};
|
||||
private int[] rateUpItems2 = {};
|
||||
private int eventChance = -1;
|
||||
private int costItem = 0;
|
||||
|
||||
public int getGachaType() {
|
||||
return gachaType;
|
||||
@ -48,8 +69,15 @@ public class GachaBanner {
|
||||
return titlePath;
|
||||
}
|
||||
|
||||
public ItemParamData getCost(int numRolls) {
|
||||
return switch (numRolls) {
|
||||
case 10 -> new ItemParamData((costItemId10 > 0) ? costItemId10 : getCostItem(), costItemAmount10);
|
||||
default -> new ItemParamData(getCostItem(), costItemAmount * numRolls);
|
||||
};
|
||||
}
|
||||
|
||||
public int getCostItem() {
|
||||
return costItem;
|
||||
return (costItem > 0) ? costItem : costItemId;
|
||||
}
|
||||
|
||||
public int getBeginTime() {
|
||||
@ -64,32 +92,42 @@ public class GachaBanner {
|
||||
return sortId;
|
||||
}
|
||||
|
||||
public int getBaseYellowWeight() {
|
||||
return baseYellowWeight;
|
||||
public int[] getRateUpItems4() {
|
||||
return (rateUpItems2.length > 0) ? rateUpItems2 : rateUpItems4;
|
||||
}
|
||||
public int[] getRateUpItems5() {
|
||||
return (rateUpItems1.length > 0) ? rateUpItems1 : rateUpItems5;
|
||||
}
|
||||
|
||||
public int getBasePurpleWeight() {
|
||||
return basePurpleWeight;
|
||||
public int[] getFallbackItems3() {return fallbackItems3;}
|
||||
public int[] getFallbackItems4Pool1() {return fallbackItems4Pool1;}
|
||||
public int[] getFallbackItems4Pool2() {return fallbackItems4Pool2;}
|
||||
public int[] getFallbackItems5Pool1() {return fallbackItems5Pool1;}
|
||||
public int[] getFallbackItems5Pool2() {return fallbackItems5Pool2;}
|
||||
|
||||
public boolean getRemoveC6FromPool() {return removeC6FromPool;}
|
||||
public boolean getAutoStripRateUpFromFallback() {return autoStripRateUpFromFallback;}
|
||||
|
||||
|
||||
public int getWeight(int rarity, int pity) {
|
||||
return switch(rarity) {
|
||||
case 4 -> Utils.lerp(pity, weights4);
|
||||
default -> Utils.lerp(pity, weights5);
|
||||
};
|
||||
}
|
||||
|
||||
public int[] getRateUpItems1() {
|
||||
return rateUpItems1;
|
||||
public int getPoolBalanceWeight(int rarity, int pity) {
|
||||
return switch(rarity) {
|
||||
case 4 -> Utils.lerp(pity, poolBalanceWeights4);
|
||||
default -> Utils.lerp(pity, poolBalanceWeights5);
|
||||
};
|
||||
}
|
||||
|
||||
public int[] getRateUpItems2() {
|
||||
return rateUpItems2;
|
||||
}
|
||||
|
||||
public int getSoftPity() {
|
||||
return softPity - 1;
|
||||
}
|
||||
|
||||
public int getHardPity() {
|
||||
return hardPity - 1;
|
||||
}
|
||||
|
||||
public int getEventChance() {
|
||||
return eventChance;
|
||||
public int getEventChance(int rarity) {
|
||||
return switch(rarity) {
|
||||
case 4 -> eventChance4;
|
||||
default -> (eventChance > -1) ? eventChance : eventChance5;
|
||||
};
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@ -102,34 +140,40 @@ public class GachaBanner {
|
||||
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
|
||||
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort)
|
||||
+ "/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);
|
||||
ItemParamData costItem1 = this.getCost(1);
|
||||
ItemParamData costItem10 = this.getCost(10);
|
||||
GachaInfo.Builder info = GachaInfo.newBuilder()
|
||||
.setGachaType(this.getGachaType())
|
||||
.setScheduleId(this.getScheduleId())
|
||||
.setBeginTime(this.getBeginTime())
|
||||
.setEndTime(this.getEndTime())
|
||||
.setCostItemId(this.getCostItem())
|
||||
.setCostItemNum(1)
|
||||
.setCostItemId(costItem1.getId())
|
||||
.setCostItemNum(costItem1.getCount())
|
||||
.setTenCostItemId(costItem10.getId())
|
||||
.setTenCostItemNum(costItem10.getCount())
|
||||
.setGachaPrefabPath(this.getPrefabPath())
|
||||
.setGachaPreviewPrefabPath(this.getPreviewPrefabPath())
|
||||
.setGachaProbUrl(record)
|
||||
.setGachaProbUrlOversea(record)
|
||||
.setGachaProbUrl(details)
|
||||
.setGachaProbUrlOversea(details)
|
||||
.setGachaRecordUrl(record)
|
||||
.setGachaRecordUrlOversea(record)
|
||||
.setTenCostItemId(this.getCostItem())
|
||||
.setTenCostItemNum(10)
|
||||
.setLeftGachaTimes(Integer.MAX_VALUE)
|
||||
.setGachaTimesLimit(Integer.MAX_VALUE)
|
||||
.setGachaSortId(this.getSortId());
|
||||
|
||||
if (this.getTitlePath() != null) {
|
||||
info.setGachaTitlePath(this.getTitlePath());
|
||||
}
|
||||
|
||||
if (this.getRateUpItems1().length > 0) {
|
||||
if (this.getRateUpItems5().length > 0) {
|
||||
GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(1);
|
||||
|
||||
for (int id : getRateUpItems1()) {
|
||||
for (int id : getRateUpItems5()) {
|
||||
upInfo.addItemIdList(id);
|
||||
info.addMainNameId(id);
|
||||
}
|
||||
@ -137,10 +181,10 @@ public class GachaBanner {
|
||||
info.addGachaUpInfoList(upInfo);
|
||||
}
|
||||
|
||||
if (this.getRateUpItems2().length > 0) {
|
||||
if (this.getRateUpItems4().length > 0) {
|
||||
GachaUpInfo.Builder upInfo = GachaUpInfo.newBuilder().setItemParentType(2);
|
||||
|
||||
for (int id : getRateUpItems2()) {
|
||||
for (int id : getRateUpItems4()) {
|
||||
upInfo.addItemIdList(id);
|
||||
if (info.getSubNameIdCount() == 0) {
|
||||
info.addSubNameId(id);
|
||||
|
@ -4,6 +4,7 @@ import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
@ -13,11 +14,12 @@ import com.google.gson.reflect.TypeToken;
|
||||
import com.sun.nio.file.SensitivityWatchEventModifier;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
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;
|
||||
import emu.grasscutter.game.inventory.Inventory;
|
||||
import emu.grasscutter.game.inventory.ItemType;
|
||||
import emu.grasscutter.game.inventory.MaterialType;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
@ -28,6 +30,7 @@ import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
|
||||
import emu.grasscutter.server.game.GameServer;
|
||||
import emu.grasscutter.server.game.GameServerTickEvent;
|
||||
import emu.grasscutter.server.packet.send.PacketDoGachaRsp;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
@ -41,15 +44,11 @@ public class GachaManager {
|
||||
private final Int2ObjectMap<GachaBanner> gachaBanners;
|
||||
private GetGachaInfoRsp cachedProto;
|
||||
WatchService watchService;
|
||||
|
||||
private final int[] yellowAvatars = new int[] {1003, 1016, 1042, 1035, 1041};
|
||||
private final int[] yellowWeapons = new int[] {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
|
||||
private final int[] purpleAvatars = new int[] {1006, 1014, 1015, 1020, 1021, 1023, 1024, 1025, 1027, 1031, 1032, 1034, 1036, 1039, 1043, 1044, 1045, 1048, 1053, 1055, 1056, 1064};
|
||||
private final int[] purpleWeapons = new int[] {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
|
||||
private final int[] blueWeapons = new int[] {11301, 11302, 11306, 12301, 12302, 12305, 13303, 14301, 14302, 14304, 15301, 15302, 15304};
|
||||
|
||||
private static final int starglitterId = 221;
|
||||
private static final int stardustId = 222;
|
||||
private int[] fallbackItems4Pool2Default = {11401, 11402, 11403, 11405, 12401, 12402, 12403, 12405, 13401, 13407, 14401, 14402, 14403, 14409, 15401, 15402, 15403, 15405};
|
||||
private int[] fallbackItems5Pool2Default = {11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502};
|
||||
|
||||
public GachaManager(GameServer server) {
|
||||
this.server = server;
|
||||
@ -66,7 +65,7 @@ public class GachaManager {
|
||||
return gachaBanners;
|
||||
}
|
||||
|
||||
public int randomRange(int min, int max) {
|
||||
public int randomRange(int min, int max) { // Both are inclusive
|
||||
return ThreadLocalRandom.current().nextInt(max - min + 1) + min;
|
||||
}
|
||||
|
||||
@ -83,6 +82,8 @@ public class GachaManager {
|
||||
getGachaBanners().put(banner.getGachaType(), banner);
|
||||
}
|
||||
Grasscutter.getLogger().info("Banners successfully loaded.");
|
||||
|
||||
|
||||
this.cachedProto = createProto();
|
||||
} else {
|
||||
Grasscutter.getLogger().error("Unable to load banners. Banners size is 0.");
|
||||
@ -92,13 +93,153 @@ public class GachaManager {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private class BannerPools {
|
||||
public int[] rateUpItems4;
|
||||
public int[] rateUpItems5;
|
||||
public int[] fallbackItems4Pool1;
|
||||
public int[] fallbackItems4Pool2;
|
||||
public int[] fallbackItems5Pool1;
|
||||
public int[] fallbackItems5Pool2;
|
||||
|
||||
public BannerPools(GachaBanner banner) {
|
||||
rateUpItems4 = banner.getRateUpItems4();
|
||||
rateUpItems5 = banner.getRateUpItems5();
|
||||
fallbackItems4Pool1 = banner.getFallbackItems4Pool1();
|
||||
fallbackItems4Pool2 = banner.getFallbackItems4Pool2();
|
||||
fallbackItems5Pool1 = banner.getFallbackItems5Pool1();
|
||||
fallbackItems5Pool2 = banner.getFallbackItems5Pool2();
|
||||
|
||||
if (banner.getAutoStripRateUpFromFallback()) {
|
||||
fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, rateUpItems4);
|
||||
fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, rateUpItems4);
|
||||
fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, rateUpItems5);
|
||||
fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, rateUpItems5);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFromAllPools(int[] itemIds) {
|
||||
rateUpItems4 = Utils.setSubtract(rateUpItems4, itemIds);
|
||||
rateUpItems5 = Utils.setSubtract(rateUpItems5, itemIds);
|
||||
fallbackItems4Pool1 = Utils.setSubtract(fallbackItems4Pool1, itemIds);
|
||||
fallbackItems4Pool2 = Utils.setSubtract(fallbackItems4Pool2, itemIds);
|
||||
fallbackItems5Pool1 = Utils.setSubtract(fallbackItems5Pool1, itemIds);
|
||||
fallbackItems5Pool2 = Utils.setSubtract(fallbackItems5Pool2, itemIds);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized int checkPlayerAvatarConstellationLevel(Player player, int itemId) { // Maybe this would be useful in the Player class?
|
||||
ItemData itemData = GameData.getItemDataMap().get(itemId);
|
||||
if ((itemData == null) || (itemData.getMaterialType() != MaterialType.MATERIAL_AVATAR)){
|
||||
return -2; // Not an Avatar
|
||||
}
|
||||
Avatar avatar = player.getAvatars().getAvatarById((itemId % 1000) + 10000000);
|
||||
if (avatar == null) {
|
||||
return -1; // Doesn't have
|
||||
}
|
||||
// Constellation
|
||||
int constLevel = avatar.getCoreProudSkillLevel();
|
||||
GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId + 100);
|
||||
constLevel += (constItem == null)? 0 : constItem.getCount();
|
||||
return constLevel;
|
||||
}
|
||||
|
||||
private synchronized int[] removeC6FromPool(int[] itemPool, Player player) {
|
||||
IntList temp = new IntArrayList();
|
||||
for (int itemId : itemPool) {
|
||||
if (checkPlayerAvatarConstellationLevel(player, itemId) < 6) {
|
||||
temp.add(itemId);
|
||||
}
|
||||
}
|
||||
return temp.toIntArray();
|
||||
}
|
||||
|
||||
private synchronized int drawRoulette(int[] weights, int cutoff) {
|
||||
// This follows the logic laid out in issue #183
|
||||
// Simple weighted selection with an upper bound for the roll that cuts off trailing entries
|
||||
// All weights must be >= 0
|
||||
int total = 0;
|
||||
for (int weight : weights) {
|
||||
if (weight < 0) {
|
||||
throw new IllegalArgumentException("Weights must be non-negative!");
|
||||
}
|
||||
total += weight;
|
||||
}
|
||||
int roll = ThreadLocalRandom.current().nextInt((total < cutoff)? total : cutoff);
|
||||
int subTotal = 0;
|
||||
for (int i=0; i<weights.length; i++) {
|
||||
subTotal += weights[i];
|
||||
if (roll < subTotal) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// throw new IllegalStateException();
|
||||
return 0; // This should only be reachable if total==0
|
||||
}
|
||||
|
||||
private synchronized int doRarePull(int[] featured, int[] fallback1, int[] fallback2, int rarity, GachaBanner banner, PlayerGachaBannerInfo gachaInfo) {
|
||||
int itemId = 0;
|
||||
boolean pullFeatured = (gachaInfo.getFailedFeaturedItemPulls(rarity) >= 1) // Lost previous coinflip
|
||||
|| (this.randomRange(1, 100) <= banner.getEventChance(rarity)); // Won this coinflip
|
||||
if (pullFeatured && (featured.length > 0)) {
|
||||
itemId = getRandom(featured);
|
||||
gachaInfo.setFailedFeaturedItemPulls(rarity, 0);
|
||||
} else {
|
||||
gachaInfo.addFailedFeaturedItemPulls(rarity, 1);
|
||||
if (fallback1.length < 1) {
|
||||
if (fallback2.length < 1) {
|
||||
itemId = getRandom((rarity==5)? fallbackItems5Pool2Default : fallbackItems4Pool2Default);
|
||||
} else {
|
||||
itemId = getRandom(fallback2);
|
||||
}
|
||||
} else if (fallback2.length < 1) {
|
||||
itemId = getRandom(fallback1);
|
||||
} else { // Both pools are possible, use the pool balancer
|
||||
int pityPool1 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 1));
|
||||
int pityPool2 = banner.getPoolBalanceWeight(rarity, gachaInfo.getPityPool(rarity, 2));
|
||||
int chosenPool = switch ((pityPool1 >= pityPool2)? 1 : 0) { // Larger weight must come first for the hard cutoff to function correctly
|
||||
case 1 -> 1 + drawRoulette(new int[] {pityPool1, pityPool2}, 10000);
|
||||
default -> 2 - drawRoulette(new int[] {pityPool2, pityPool1}, 10000);
|
||||
};
|
||||
itemId = switch (chosenPool) {
|
||||
case 1:
|
||||
gachaInfo.setPityPool(rarity, 1, 0);
|
||||
yield getRandom(fallback1);
|
||||
default:
|
||||
gachaInfo.setPityPool(rarity, 2, 0);
|
||||
yield getRandom(fallback2);
|
||||
};
|
||||
}
|
||||
}
|
||||
return itemId;
|
||||
}
|
||||
|
||||
private synchronized int doPull(GachaBanner banner, PlayerGachaBannerInfo gachaInfo, BannerPools pools) {
|
||||
// Pre-increment all pity pools (yes this makes all calculations assume 1-indexed pity)
|
||||
gachaInfo.incPityAll();
|
||||
|
||||
int[] weights = {banner.getWeight(5, gachaInfo.getPity5()), banner.getWeight(4, gachaInfo.getPity4()), 10000};
|
||||
int levelWon = 5 - drawRoulette(weights, 10000);
|
||||
|
||||
return switch (levelWon) {
|
||||
case 5:
|
||||
gachaInfo.setPity5(0);
|
||||
yield doRarePull(pools.rateUpItems5, pools.fallbackItems5Pool1, pools.fallbackItems5Pool2, 5, banner, gachaInfo);
|
||||
case 4:
|
||||
gachaInfo.setPity4(0);
|
||||
yield doRarePull(pools.rateUpItems4, pools.fallbackItems4Pool1, pools.fallbackItems4Pool2, 4, banner, gachaInfo);
|
||||
default:
|
||||
yield getRandom(banner.getFallbackItems3());
|
||||
};
|
||||
}
|
||||
|
||||
public synchronized void doPulls(Player player, int gachaType, int times) {
|
||||
// Sanity check
|
||||
if (times != 10 && times != 1) {
|
||||
return;
|
||||
}
|
||||
if (player.getInventory().getInventoryTab(ItemType.ITEM_WEAPON).getSize() + times > player.getInventory().getInventoryTab(ItemType.ITEM_WEAPON).getMaxCapacity()) {
|
||||
Inventory inventory = player.getInventory();
|
||||
if (inventory.getInventoryTab(ItemType.ITEM_WEAPON).getSize() + times > inventory.getInventoryTab(ItemType.ITEM_WEAPON).getMaxCapacity()) {
|
||||
player.sendPacket(new PacketDoGachaRsp());
|
||||
return;
|
||||
}
|
||||
@ -111,93 +252,33 @@ public class GachaManager {
|
||||
}
|
||||
|
||||
// Spend currency
|
||||
if (banner.getCostItem() > 0) {
|
||||
GameItem costItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(banner.getCostItem());
|
||||
if (costItem == null || costItem.getCount() < times) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.getInventory().removeItem(costItem, times);
|
||||
}
|
||||
|
||||
// Roll
|
||||
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner);
|
||||
IntList wonItems = new IntArrayList(times);
|
||||
|
||||
for (int i = 0; i < times; i++) {
|
||||
int random = this.randomRange(1, 10000);
|
||||
int itemId = 0;
|
||||
|
||||
int bonusYellowChance = gachaInfo.getPity5() >= banner.getSoftPity() ? 100 * (gachaInfo.getPity5() - banner.getSoftPity() - 1): 0;
|
||||
int yellowChance = banner.getBaseYellowWeight() + (int) Math.floor(100f * (gachaInfo.getPity5() / (banner.getSoftPity() - 1D))) + bonusYellowChance;
|
||||
int purpleChance = 10000 - (banner.getBasePurpleWeight() + (int) Math.floor(790f * (gachaInfo.getPity4() / 8f)));
|
||||
|
||||
if (random <= yellowChance || gachaInfo.getPity5() >= banner.getHardPity()) {
|
||||
if (banner.getRateUpItems1().length > 0) {
|
||||
int eventChance = this.randomRange(1, 100);
|
||||
|
||||
if (eventChance <= banner.getEventChance() || gachaInfo.getFailedFeaturedItemPulls() >= 1) {
|
||||
itemId = getRandom(banner.getRateUpItems1());
|
||||
gachaInfo.setFailedFeaturedItemPulls(0);
|
||||
} else {
|
||||
// Lost the 50/50... rip
|
||||
gachaInfo.addFailedFeaturedItemPulls(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (itemId == 0) {
|
||||
int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2);
|
||||
if (typeChance == 1) {
|
||||
itemId = getRandom(this.yellowAvatars);
|
||||
} else {
|
||||
itemId = getRandom(this.yellowWeapons);
|
||||
}
|
||||
}
|
||||
|
||||
// Pity
|
||||
gachaInfo.addPity4(1);
|
||||
gachaInfo.setPity5(0);
|
||||
} else if (random >= purpleChance || gachaInfo.getPity4() >= 9) {
|
||||
if (banner.getRateUpItems2().length > 0) {
|
||||
int eventChance = this.randomRange(1, 100);
|
||||
|
||||
if (eventChance >= 50) {
|
||||
itemId = getRandom(banner.getRateUpItems2());
|
||||
}
|
||||
}
|
||||
|
||||
if (itemId == 0) {
|
||||
int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2);
|
||||
if (typeChance == 1) {
|
||||
itemId = getRandom(this.purpleAvatars);
|
||||
} else {
|
||||
itemId = getRandom(this.purpleWeapons);
|
||||
}
|
||||
}
|
||||
|
||||
// Pity
|
||||
gachaInfo.addPity5(1);
|
||||
gachaInfo.setPity4(0);
|
||||
} else {
|
||||
itemId = getRandom(this.blueWeapons);
|
||||
|
||||
// Pity
|
||||
gachaInfo.addPity4(1);
|
||||
gachaInfo.addPity5(1);
|
||||
}
|
||||
|
||||
// Add winning item
|
||||
wonItems.add(itemId);
|
||||
ItemParamData cost = banner.getCost(times);
|
||||
if (cost.getCount() > 0 && !inventory.payItem(cost)) {
|
||||
player.sendPacket(new PacketDoGachaRsp());
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to character
|
||||
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner);
|
||||
BannerPools pools = new BannerPools(banner);
|
||||
List<GachaItem> list = new ArrayList<>();
|
||||
int stardust = 0, starglitter = 0;
|
||||
|
||||
if (banner.getRemoveC6FromPool()) { // The ultimate form of pity (non-vanilla)
|
||||
pools.rateUpItems4 = removeC6FromPool(pools.rateUpItems4, player);
|
||||
pools.rateUpItems5 = removeC6FromPool(pools.rateUpItems5, player);
|
||||
pools.fallbackItems4Pool1 = removeC6FromPool(pools.fallbackItems4Pool1, player);
|
||||
pools.fallbackItems4Pool2 = removeC6FromPool(pools.fallbackItems4Pool2, player);
|
||||
pools.fallbackItems5Pool1 = removeC6FromPool(pools.fallbackItems5Pool1, player);
|
||||
pools.fallbackItems5Pool2 = removeC6FromPool(pools.fallbackItems5Pool2, player);
|
||||
}
|
||||
|
||||
for (int itemId : wonItems) {
|
||||
for (int i = 0; i < times; i++) {
|
||||
// Roll
|
||||
int itemId = doPull(banner, gachaInfo, pools);
|
||||
ItemData itemData = GameData.getItemDataMap().get(itemId);
|
||||
if (itemData == null) {
|
||||
continue;
|
||||
continue; // Maybe we should bail out if an item fails instead of rolling the rest?
|
||||
}
|
||||
|
||||
// Write gacha record
|
||||
@ -210,57 +291,47 @@ public class GachaManager {
|
||||
boolean isTransferItem = false;
|
||||
|
||||
// Const check
|
||||
if (itemData.getMaterialType() == MaterialType.MATERIAL_AVATAR) {
|
||||
int avatarId = (itemData.getId() % 1000) + 10000000;
|
||||
Avatar avatar = player.getAvatars().getAvatarById(avatarId);
|
||||
if (avatar != null) {
|
||||
int constLevel = avatar.getCoreProudSkillLevel();
|
||||
int constItemId = itemData.getId() + 100;
|
||||
GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId);
|
||||
if (constItem != null) {
|
||||
constLevel += constItem.getCount();
|
||||
int constellation = checkPlayerAvatarConstellationLevel(player, itemId);
|
||||
switch (constellation) {
|
||||
case -2: // Is weapon
|
||||
switch (itemData.getRankLevel()) {
|
||||
case 5 -> addStarglitter = 10;
|
||||
case 4 -> addStarglitter = 2;
|
||||
default -> addStardust = 15;
|
||||
}
|
||||
|
||||
if (constLevel < 6) {
|
||||
// Not max const
|
||||
addStarglitter = 2;
|
||||
// Add 1 const
|
||||
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null));
|
||||
player.getInventory().addItem(constItemId, 1);
|
||||
} else {
|
||||
// Is max const
|
||||
addStarglitter = 5;
|
||||
}
|
||||
|
||||
if (itemData.getRankLevel() == 5) {
|
||||
addStarglitter *= 5;
|
||||
}
|
||||
|
||||
isTransferItem = true;
|
||||
} else {
|
||||
// New
|
||||
break;
|
||||
case -1: // New character
|
||||
gachaItem.setIsGachaItemNew(true);
|
||||
}
|
||||
} else {
|
||||
// Is weapon
|
||||
switch (itemData.getRankLevel()) {
|
||||
case 5 -> addStarglitter = 10;
|
||||
case 4 -> addStarglitter = 2;
|
||||
case 3 -> addStardust = 15;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (constellation >= 6) { // C6, give consolation starglitter
|
||||
addStarglitter = (itemData.getRankLevel()==5)? 25 : 5;
|
||||
} else { // C0-C5, give constellation item
|
||||
if (banner.getRemoveC6FromPool() && constellation == 5) { // New C6, remove it from the pools so we don't get C7 in a 10pull
|
||||
pools.removeFromAllPools(new int[] {itemId});
|
||||
}
|
||||
addStarglitter = (itemData.getRankLevel()==5)? 10 : 2;
|
||||
int constItemId = itemId + 100;
|
||||
GameItem constItem = inventory.getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId);
|
||||
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null));
|
||||
inventory.addItem(constItemId, 1);
|
||||
}
|
||||
isTransferItem = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Create item
|
||||
GameItem item = new GameItem(itemData);
|
||||
gachaItem.setGachaItem(item.toItemParam());
|
||||
player.getInventory().addItem(item);
|
||||
inventory.addItem(item);
|
||||
|
||||
stardust += addStardust;
|
||||
starglitter += addStarglitter;
|
||||
|
||||
if (addStardust > 0) {
|
||||
gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(stardustId).setCount(addStardust));
|
||||
} if (addStarglitter > 0) {
|
||||
}
|
||||
if (addStarglitter > 0) {
|
||||
ItemParam starglitterParam = ItemParam.newBuilder().setItemId(starglitterId).setCount(addStarglitter).build();
|
||||
if (isTransferItem) {
|
||||
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(starglitterParam));
|
||||
@ -273,9 +344,10 @@ public class GachaManager {
|
||||
|
||||
// Add stardust/starglitter
|
||||
if (stardust > 0) {
|
||||
player.getInventory().addItem(stardustId, stardust);
|
||||
} if (starglitter > 0) {
|
||||
player.getInventory().addItem(starglitterId, starglitter);
|
||||
inventory.addItem(stardustId, stardust);
|
||||
}
|
||||
if (starglitter > 0) {
|
||||
inventory.addItem(starglitterId, starglitter);
|
||||
}
|
||||
|
||||
// Packets
|
||||
|
@ -7,6 +7,11 @@ public class PlayerGachaBannerInfo {
|
||||
private int pity5 = 0;
|
||||
private int pity4 = 0;
|
||||
private int failedFeaturedItemPulls = 0;
|
||||
private int failedFeatured4ItemPulls = 0;
|
||||
private int pity5Pool1 = 0;
|
||||
private int pity5Pool2 = 0;
|
||||
private int pity4Pool1 = 0;
|
||||
private int pity4Pool2 = 0;
|
||||
|
||||
public int getPity5() {
|
||||
return pity5;
|
||||
@ -32,15 +37,82 @@ public class PlayerGachaBannerInfo {
|
||||
this.pity4 += amount;
|
||||
}
|
||||
|
||||
public int getFailedFeaturedItemPulls() {
|
||||
return failedFeaturedItemPulls;
|
||||
public int getFailedFeaturedItemPulls(int rarity) {
|
||||
return switch (rarity) {
|
||||
case 4 -> failedFeatured4ItemPulls;
|
||||
default -> failedFeaturedItemPulls; // 5
|
||||
};
|
||||
}
|
||||
|
||||
public void setFailedFeaturedItemPulls(int failedEventCharacterPulls) {
|
||||
this.failedFeaturedItemPulls = failedEventCharacterPulls;
|
||||
public void setFailedFeaturedItemPulls(int rarity, int amount) {
|
||||
switch (rarity) {
|
||||
case 4 -> failedFeatured4ItemPulls = amount;
|
||||
default -> failedFeaturedItemPulls = amount; // 5
|
||||
};
|
||||
}
|
||||
|
||||
public void addFailedFeaturedItemPulls(int amount) {
|
||||
failedFeaturedItemPulls += amount;
|
||||
public void addFailedFeaturedItemPulls(int rarity, int amount) {
|
||||
switch (rarity) {
|
||||
case 4 -> failedFeatured4ItemPulls += amount;
|
||||
default -> failedFeaturedItemPulls += amount; // 5
|
||||
};
|
||||
}
|
||||
|
||||
public int getPityPool(int rarity, int pool) {
|
||||
return switch (rarity) {
|
||||
case 4 -> switch (pool) {
|
||||
case 1 -> pity4Pool1;
|
||||
default -> pity4Pool2;
|
||||
};
|
||||
default -> switch (pool) {
|
||||
case 1 -> pity5Pool1;
|
||||
default -> pity5Pool2;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
public void setPityPool(int rarity, int pool, int amount) {
|
||||
switch (rarity) {
|
||||
case 4:
|
||||
switch (pool) {
|
||||
case 1 -> pity4Pool1 = amount;
|
||||
default -> pity4Pool2 = amount;
|
||||
};
|
||||
break;
|
||||
case 5:
|
||||
default:
|
||||
switch (pool) {
|
||||
case 1 -> pity5Pool1 = amount;
|
||||
default -> pity5Pool2 = amount;
|
||||
};
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
public void addPityPool(int rarity, int pool, int amount) {
|
||||
switch (rarity) {
|
||||
case 4:
|
||||
switch (pool) {
|
||||
case 1 -> pity4Pool1 += amount;
|
||||
default -> pity4Pool2 += amount;
|
||||
};
|
||||
break;
|
||||
case 5:
|
||||
default:
|
||||
switch (pool) {
|
||||
case 1 -> pity5Pool1 += amount;
|
||||
default -> pity5Pool2 += amount;
|
||||
};
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
public void incPityAll() {
|
||||
pity4++;
|
||||
pity5++;
|
||||
pity4Pool1++;
|
||||
pity4Pool2++;
|
||||
pity5Pool1++;
|
||||
pity5Pool2++;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import java.util.List;
|
||||
|
||||
import emu.grasscutter.GameConstants;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.def.AvatarCostumeData;
|
||||
import emu.grasscutter.data.def.AvatarData;
|
||||
import emu.grasscutter.data.def.AvatarFlycloakData;
|
||||
@ -256,6 +257,64 @@ public class Inventory implements Iterable<GameItem> {
|
||||
getPlayer().setCrystals(player.getCrystals() + count);
|
||||
}
|
||||
}
|
||||
|
||||
private int getVirtualItemCount(int itemId) {
|
||||
switch (itemId) {
|
||||
case 201: // Primogem
|
||||
return player.getPrimogems();
|
||||
case 202: // Mora
|
||||
return player.getMora();
|
||||
case 203: // Genesis Crystals
|
||||
return player.getCrystals();
|
||||
default:
|
||||
GameItem item = getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId); // What if we ever want to operate on weapons/relics/furniture? :S
|
||||
return (item == null) ? 0 : item.getCount();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean payItem(int id, int count) {
|
||||
return payItem(new ItemParamData(id, count));
|
||||
}
|
||||
|
||||
public boolean payItem(ItemParamData costItem) {
|
||||
return payItems(new ItemParamData[] {costItem}, 1, null);
|
||||
}
|
||||
|
||||
public boolean payItems(ItemParamData[] costItems) {
|
||||
return payItems(costItems, 1, null);
|
||||
}
|
||||
|
||||
public boolean payItems(ItemParamData[] costItems, int quantity) {
|
||||
return payItems(costItems, quantity, null);
|
||||
}
|
||||
|
||||
public synchronized boolean payItems(ItemParamData[] costItems, int quantity, ActionReason reason) {
|
||||
// Make sure player has requisite items
|
||||
for (ItemParamData cost : costItems) {
|
||||
if (getVirtualItemCount(cost.getId()) < (cost.getCount() * quantity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// All costs are satisfied, now remove them all
|
||||
for (ItemParamData cost : costItems) {
|
||||
switch (cost.getId()) {
|
||||
case 201 -> // Primogem
|
||||
player.setPrimogems(player.getPrimogems() - (cost.getCount() * quantity));
|
||||
case 202 -> // Mora
|
||||
player.setMora(player.getMora() - (cost.getCount() * quantity));
|
||||
case 203 -> // Genesis Crystals
|
||||
player.setCrystals(player.getCrystals() - (cost.getCount() * quantity));
|
||||
default ->
|
||||
removeItem(getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId()), cost.getCount() * quantity);
|
||||
}
|
||||
}
|
||||
|
||||
if (reason != null) { // Do we need these?
|
||||
// getPlayer().sendPacket(new PacketItemAddHintNotify(changedItems, reason));
|
||||
}
|
||||
// getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void removeItems(List<GameItem> items) {
|
||||
// TODO Bulk delete
|
||||
|
@ -1,6 +1,7 @@
|
||||
package emu.grasscutter.game.managers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@ -38,6 +39,8 @@ public class InventoryManager {
|
||||
|
||||
private final static int RELIC_MATERIAL_1 = 105002; // Sanctifying Unction
|
||||
private final static int RELIC_MATERIAL_2 = 105003; // Sanctifying Essence
|
||||
private final static int RELIC_MATERIAL_EXP_1 = 2500; // Sanctifying Unction
|
||||
private final static int RELIC_MATERIAL_EXP_2 = 10000; // Sanctifying Essence
|
||||
|
||||
private final static int WEAPON_ORE_1 = 104011; // Enhancement Ore
|
||||
private final static int WEAPON_ORE_2 = 104012; // Fine Enhancement Ore
|
||||
@ -85,6 +88,7 @@ public class InventoryManager {
|
||||
int moraCost = 0;
|
||||
int expGain = 0;
|
||||
|
||||
List<GameItem> foodRelics = new ArrayList<GameItem>();
|
||||
for (long guid : foodRelicList) {
|
||||
// Add to delete queue
|
||||
GameItem food = player.getInventory().getItemByGuid(guid);
|
||||
@ -96,23 +100,21 @@ public class InventoryManager {
|
||||
expGain += food.getItemData().getBaseConvExp();
|
||||
// Feeding artifact with exp already
|
||||
if (food.getTotalExp() > 0) {
|
||||
expGain += (int) Math.floor(food.getTotalExp() * .8f);
|
||||
expGain += (food.getTotalExp() * 4) / 5;
|
||||
}
|
||||
foodRelics.add(food);
|
||||
}
|
||||
List<ItemParamData> payList = new ArrayList<ItemParamData>();
|
||||
for (ItemParam itemParam : list) {
|
||||
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemParam.getItemId());
|
||||
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_RELIQUARY_MATERIAL) {
|
||||
continue;
|
||||
}
|
||||
int amount = Math.min(food.getCount(), itemParam.getCount());
|
||||
int gain = 0;
|
||||
if (food.getItemId() == RELIC_MATERIAL_2) {
|
||||
gain = 10000 * amount;
|
||||
} else if (food.getItemId() == RELIC_MATERIAL_1) {
|
||||
gain = 2500 * amount;
|
||||
}
|
||||
int amount = itemParam.getCount(); // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
|
||||
int gain = amount * switch(itemParam.getItemId()) {
|
||||
case RELIC_MATERIAL_1 -> RELIC_MATERIAL_EXP_1;
|
||||
case RELIC_MATERIAL_2 -> RELIC_MATERIAL_EXP_2;
|
||||
default -> 0;
|
||||
};
|
||||
expGain += gain;
|
||||
moraCost += gain;
|
||||
payList.add(new ItemParamData(itemParam.getItemId(), itemParam.getCount()));
|
||||
}
|
||||
|
||||
// Make sure exp gain is valid
|
||||
@ -120,28 +122,14 @@ public class InventoryManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check mora
|
||||
if (player.getMora() < moraCost) {
|
||||
// Confirm payment of materials and mora (assume food relics are payable afterwards)
|
||||
payList.add(new ItemParamData(202, moraCost));
|
||||
if (!player.getInventory().payItems(payList.toArray(new ItemParamData[0]))) {
|
||||
return;
|
||||
}
|
||||
player.setMora(player.getMora() - moraCost);
|
||||
|
||||
// Consume food items
|
||||
for (long guid : foodRelicList) {
|
||||
GameItem food = player.getInventory().getItemByGuid(guid);
|
||||
if (food == null || !food.isDestroyable()) {
|
||||
continue;
|
||||
}
|
||||
player.getInventory().removeItem(food);
|
||||
}
|
||||
for (ItemParam itemParam : list) {
|
||||
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemParam.getItemId());
|
||||
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_RELIQUARY_MATERIAL) {
|
||||
continue;
|
||||
}
|
||||
int amount = Math.min(food.getCount(), itemParam.getCount());
|
||||
player.getInventory().removeItem(food, amount);
|
||||
}
|
||||
// Consume food relics
|
||||
player.getInventory().removeItems(foodRelics);
|
||||
|
||||
// Implement random rate boost
|
||||
int rate = 1;
|
||||
@ -231,22 +219,16 @@ public class InventoryManager {
|
||||
}
|
||||
expGain += food.getItemData().getWeaponBaseExp();
|
||||
if (food.getTotalExp() > 0) {
|
||||
expGain += (int) Math.floor(food.getTotalExp() * .8f);
|
||||
expGain += (food.getTotalExp() * 4) / 5;
|
||||
}
|
||||
}
|
||||
for (ItemParam param : itemParamList) {
|
||||
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId());
|
||||
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) {
|
||||
continue;
|
||||
}
|
||||
int amount = Math.min(param.getCount(), food.getCount());
|
||||
if (food.getItemId() == WEAPON_ORE_3) {
|
||||
expGain += 10000 * amount;
|
||||
} else if (food.getItemId() == WEAPON_ORE_2) {
|
||||
expGain += 2000 * amount;
|
||||
} else if (food.getItemId() == WEAPON_ORE_1) {
|
||||
expGain += 400 * amount;
|
||||
}
|
||||
expGain += param.getCount() * switch(param.getItemId()) {
|
||||
case WEAPON_ORE_1 -> WEAPON_ORE_EXP_1;
|
||||
case WEAPON_ORE_2 -> WEAPON_ORE_EXP_2;
|
||||
case WEAPON_ORE_3 -> WEAPON_ORE_EXP_3;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
// Try
|
||||
@ -288,65 +270,45 @@ public class InventoryManager {
|
||||
}
|
||||
|
||||
// Get exp gain
|
||||
int expGain = 0, moraCost = 0;
|
||||
|
||||
int expGain = 0, expGainFree = 0;
|
||||
List<GameItem> foodWeapons = new ArrayList<GameItem>();
|
||||
for (long guid : foodWeaponGuidList) {
|
||||
GameItem food = player.getInventory().getItemByGuid(guid);
|
||||
if (food == null || !food.isDestroyable()) {
|
||||
continue;
|
||||
}
|
||||
expGain += food.getItemData().getWeaponBaseExp();
|
||||
moraCost += (int) Math.floor(food.getItemData().getWeaponBaseExp() * .1f);
|
||||
if (food.getTotalExp() > 0) {
|
||||
expGain += (int) Math.floor(food.getTotalExp() * .8f);
|
||||
expGainFree += (food.getTotalExp() * 4) / 5; // No tax :D
|
||||
}
|
||||
foodWeapons.add(food);
|
||||
}
|
||||
List<ItemParamData> payList = new ArrayList<ItemParamData>();
|
||||
for (ItemParam param : itemParamList) {
|
||||
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId());
|
||||
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) {
|
||||
continue;
|
||||
}
|
||||
int amount = Math.min(param.getCount(), food.getCount());
|
||||
int gain = 0;
|
||||
if (food.getItemId() == WEAPON_ORE_3) {
|
||||
gain = 10000 * amount;
|
||||
} else if (food.getItemId() == WEAPON_ORE_2) {
|
||||
gain = 2000 * amount;
|
||||
} else if (food.getItemId() == WEAPON_ORE_1) {
|
||||
gain = 400 * amount;
|
||||
}
|
||||
int amount = param.getCount(); // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
|
||||
int gain = amount * switch(param.getItemId()) {
|
||||
case WEAPON_ORE_1 -> WEAPON_ORE_EXP_1;
|
||||
case WEAPON_ORE_2 -> WEAPON_ORE_EXP_2;
|
||||
case WEAPON_ORE_3 -> WEAPON_ORE_EXP_3;
|
||||
default -> 0;
|
||||
};
|
||||
expGain += gain;
|
||||
moraCost += (int) Math.floor(gain * .1f);
|
||||
payList.add(new ItemParamData(param.getItemId(), amount));
|
||||
}
|
||||
|
||||
// Make sure exp gain is valid
|
||||
int moraCost = expGain / 10;
|
||||
expGain += expGainFree;
|
||||
if (expGain <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mora check
|
||||
if (player.getMora() >= moraCost) {
|
||||
player.setMora(player.getMora() - moraCost);
|
||||
} else {
|
||||
|
||||
// Confirm payment of materials and mora (assume food weapons are payable afterwards)
|
||||
payList.add(new ItemParamData(202, moraCost));
|
||||
if (!player.getInventory().payItems(payList.toArray(new ItemParamData[0]))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume weapon/items used to feed
|
||||
for (long guid : foodWeaponGuidList) {
|
||||
GameItem food = player.getInventory().getItemByGuid(guid);
|
||||
if (food == null || !food.isDestroyable()) {
|
||||
continue;
|
||||
}
|
||||
player.getInventory().removeItem(food);
|
||||
}
|
||||
for (ItemParam param : itemParamList) {
|
||||
GameItem food = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(param.getItemId());
|
||||
if (food == null || food.getItemData().getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) {
|
||||
continue;
|
||||
}
|
||||
int amount = Math.min(param.getCount(), food.getCount());
|
||||
player.getInventory().removeItem(food, amount);
|
||||
}
|
||||
player.getInventory().removeItems(foodWeapons);
|
||||
|
||||
// Level up
|
||||
int maxLevel = promoteData.getUnlockMaxLevel();
|
||||
@ -393,7 +355,7 @@ public class InventoryManager {
|
||||
player.sendPacket(new PacketWeaponUpgradeRsp(weapon, oldLevel, leftovers));
|
||||
}
|
||||
|
||||
private List<ItemParam> getLeftoverOres(float leftover) {
|
||||
private List<ItemParam> getLeftoverOres(int leftover) {
|
||||
List<ItemParam> leftoverOreList = new ArrayList<>(3);
|
||||
|
||||
if (leftover < WEAPON_ORE_EXP_1) {
|
||||
@ -401,11 +363,11 @@ public class InventoryManager {
|
||||
}
|
||||
|
||||
// Get leftovers
|
||||
int ore3 = (int) Math.floor(leftover / WEAPON_ORE_EXP_3);
|
||||
int ore3 = leftover / WEAPON_ORE_EXP_3;
|
||||
leftover = leftover % WEAPON_ORE_EXP_3;
|
||||
int ore2 = (int) Math.floor(leftover / WEAPON_ORE_EXP_2);
|
||||
int ore2 = leftover / WEAPON_ORE_EXP_2;
|
||||
leftover = leftover % WEAPON_ORE_EXP_2;
|
||||
int ore1 = (int) Math.floor(leftover / WEAPON_ORE_EXP_1);
|
||||
int ore1 = leftover / WEAPON_ORE_EXP_1;
|
||||
|
||||
if (ore3 > 0) {
|
||||
leftoverOreList.add(ItemParam.newBuilder().setItemId(WEAPON_ORE_3).setCount(ore3).build());
|
||||
@ -496,27 +458,16 @@ public class InventoryManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure player has promote items
|
||||
for (ItemParamData cost : nextPromoteData.getCostItems()) {
|
||||
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
|
||||
if (feedItem == null || feedItem.getCount() < cost.getCount()) {
|
||||
return;
|
||||
}
|
||||
// Pay materials and mora if possible
|
||||
ItemParamData[] costs = nextPromoteData.getCostItems(); // Can this be null?
|
||||
if (nextPromoteData.getCoinCost() > 0) {
|
||||
costs = Arrays.copyOf(costs, costs.length + 1);
|
||||
costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
|
||||
}
|
||||
|
||||
// Mora check
|
||||
if (player.getMora() >= nextPromoteData.getCoinCost()) {
|
||||
player.setMora(player.getMora() - nextPromoteData.getCoinCost());
|
||||
} else {
|
||||
if (!player.getInventory().payItems(costs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume promote filler items
|
||||
for (ItemParamData cost : nextPromoteData.getCostItems()) {
|
||||
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
|
||||
player.getInventory().removeItem(feedItem, cost.getCount());
|
||||
}
|
||||
|
||||
int oldPromoteLevel = weapon.getPromoteLevel();
|
||||
weapon.setPromoteLevel(nextPromoteLevel);
|
||||
weapon.save();
|
||||
@ -552,27 +503,16 @@ public class InventoryManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure player has cost items
|
||||
for (ItemParamData cost : nextPromoteData.getCostItems()) {
|
||||
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
|
||||
if (feedItem == null || feedItem.getCount() < cost.getCount()) {
|
||||
return;
|
||||
}
|
||||
// Pay materials and mora if possible
|
||||
ItemParamData[] costs = nextPromoteData.getCostItems(); // Can this be null?
|
||||
if (nextPromoteData.getCoinCost() > 0) {
|
||||
costs = Arrays.copyOf(costs, costs.length + 1);
|
||||
costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
|
||||
}
|
||||
|
||||
// Mora check
|
||||
if (player.getMora() >= nextPromoteData.getCoinCost()) {
|
||||
player.setMora(player.getMora() - nextPromoteData.getCoinCost());
|
||||
} else {
|
||||
if (!player.getInventory().payItems(costs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume promote filler items
|
||||
for (ItemParamData cost : nextPromoteData.getCostItems()) {
|
||||
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
|
||||
player.getInventory().removeItem(feedItem, cost.getCount());
|
||||
}
|
||||
|
||||
// Update promote level
|
||||
avatar.setPromoteLevel(nextPromoteLevel);
|
||||
|
||||
@ -616,34 +556,25 @@ public class InventoryManager {
|
||||
return;
|
||||
}
|
||||
|
||||
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId);
|
||||
|
||||
if (feedItem == null || feedItem.getItemData().getMaterialType() != MaterialType.MATERIAL_EXP_FRUIT || feedItem.getCount() < count) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calc exp
|
||||
int expGain = 0, moraCost = 0;
|
||||
int expGain = switch(itemId) {
|
||||
case AVATAR_BOOK_1 -> AVATAR_BOOK_EXP_1 * count;
|
||||
case AVATAR_BOOK_2 -> AVATAR_BOOK_EXP_2 * count;
|
||||
case AVATAR_BOOK_3 -> AVATAR_BOOK_EXP_3 * count;
|
||||
default -> 0;
|
||||
};
|
||||
|
||||
// TODO clean up
|
||||
if (itemId == AVATAR_BOOK_3) {
|
||||
expGain = AVATAR_BOOK_EXP_3 * count;
|
||||
} else if (itemId == AVATAR_BOOK_2) {
|
||||
expGain = AVATAR_BOOK_EXP_2 * count;
|
||||
} else if (itemId == AVATAR_BOOK_1) {
|
||||
expGain = AVATAR_BOOK_EXP_1 * count;
|
||||
}
|
||||
moraCost = (int) Math.floor(expGain * .2f);
|
||||
|
||||
// Mora check
|
||||
if (player.getMora() >= moraCost) {
|
||||
player.setMora(player.getMora() - moraCost);
|
||||
} else {
|
||||
// Sanity check
|
||||
if (expGain <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Payment check
|
||||
int moraCost = expGain / 5;
|
||||
ItemParamData[] costItems = new ItemParamData[] {new ItemParamData(itemId, count), new ItemParamData(202, moraCost)};
|
||||
if (!player.getInventory().payItems(costItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume items
|
||||
player.getInventory().removeItem(feedItem, count);
|
||||
|
||||
// Level up
|
||||
upgradeAvatar(player, avatar, promoteData, expGain);
|
||||
@ -764,33 +695,15 @@ public class InventoryManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure player has cost items
|
||||
for (ItemParamData cost : proudSkill.getCostItems()) {
|
||||
if (cost.getId() == 0) {
|
||||
continue;
|
||||
}
|
||||
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
|
||||
if (feedItem == null || feedItem.getCount() < cost.getCount()) {
|
||||
return;
|
||||
}
|
||||
// Pay materials and mora if possible
|
||||
List<ItemParamData> costs = new ArrayList<ItemParamData>(proudSkill.getCostItems()); // Can this be null?
|
||||
if (proudSkill.getCoinCost() > 0) {
|
||||
costs.add(new ItemParamData(202, proudSkill.getCoinCost()));
|
||||
}
|
||||
|
||||
// Mora check
|
||||
if (player.getMora() >= proudSkill.getCoinCost()) {
|
||||
player.setMora(player.getMora() - proudSkill.getCoinCost());
|
||||
} else {
|
||||
if (!player.getInventory().payItems(costs.toArray(new ItemParamData[0]))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume promote filler items
|
||||
for (ItemParamData cost : proudSkill.getCostItems()) {
|
||||
if (cost.getId() == 0) {
|
||||
continue;
|
||||
}
|
||||
GameItem feedItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(cost.getId());
|
||||
player.getInventory().removeItem(feedItem, cost.getCount());
|
||||
}
|
||||
|
||||
// Upgrade skill
|
||||
avatar.getSkillLevelMap().put(skillId, nextLevel);
|
||||
avatar.save();
|
||||
@ -822,14 +735,11 @@ public class InventoryManager {
|
||||
return;
|
||||
}
|
||||
|
||||
GameItem costItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(talentData.getMainCostItemId());
|
||||
if (costItem == null || costItem.getCount() < talentData.getMainCostItemCount()) {
|
||||
// Pay constellation item if possible
|
||||
if (!player.getInventory().payItem(talentData.getMainCostItemId(), 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume item
|
||||
player.getInventory().removeItem(costItem, talentData.getMainCostItemCount());
|
||||
|
||||
// Apply + recalc
|
||||
avatar.getTalentIdList().add(talentData.getId());
|
||||
avatar.setCoreProudSkillLevel(currentTalentLevel + 1);
|
||||
|
@ -1,29 +1,29 @@
|
||||
package emu.grasscutter.game.managers.MapMarkManager;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import emu.grasscutter.net.proto.MapMarkFromTypeOuterClass;
|
||||
import emu.grasscutter.net.proto.MapMarkPointOuterClass;
|
||||
import emu.grasscutter.net.proto.MapMarkPointTypeOuterClass;
|
||||
import emu.grasscutter.net.proto.MapMarkFromTypeOuterClass.MapMarkFromType;
|
||||
import emu.grasscutter.net.proto.MapMarkPointOuterClass.MapMarkPoint;
|
||||
import emu.grasscutter.net.proto.MapMarkPointTypeOuterClass.MapMarkPointType;
|
||||
import emu.grasscutter.utils.Position;
|
||||
|
||||
@Entity
|
||||
public class MapMark {
|
||||
private int sceneId;
|
||||
private String name;
|
||||
private Position position;
|
||||
private MapMarkPointTypeOuterClass.MapMarkPointType pointType;
|
||||
private int monsterId = 0;
|
||||
private MapMarkFromTypeOuterClass.MapMarkFromType fromType;
|
||||
private int questId = 7;
|
||||
private final int sceneId;
|
||||
private final String name;
|
||||
private final Position position;
|
||||
private final MapMarkPointType pointType;
|
||||
private final int monsterId;
|
||||
private final MapMarkFromType fromType;
|
||||
private final int questId;
|
||||
|
||||
public MapMark(Position position, MapMarkPointTypeOuterClass.MapMarkPointType type) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public MapMark(MapMarkPointOuterClass.MapMarkPoint mapMarkPoint) {
|
||||
public MapMark(MapMarkPoint mapMarkPoint) {
|
||||
this.sceneId = mapMarkPoint.getSceneId();
|
||||
this.name = mapMarkPoint.getName();
|
||||
this.position = new Position(mapMarkPoint.getPos().getX(), mapMarkPoint.getPos().getY(), mapMarkPoint.getPos().getZ());
|
||||
this.position = new Position(
|
||||
mapMarkPoint.getPos().getX(),
|
||||
mapMarkPoint.getPos().getY(),
|
||||
mapMarkPoint.getPos().getZ()
|
||||
);
|
||||
this.pointType = mapMarkPoint.getPointType();
|
||||
this.monsterId = mapMarkPoint.getMonsterId();
|
||||
this.fromType = mapMarkPoint.getFromType();
|
||||
@ -33,41 +33,22 @@ public class MapMark {
|
||||
public int getSceneId() {
|
||||
return this.sceneId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public Position getPosition() {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
public MapMarkPointTypeOuterClass.MapMarkPointType getMapMarkPointType() {
|
||||
public MapMarkPointType getMapMarkPointType() {
|
||||
return this.pointType;
|
||||
}
|
||||
|
||||
public void setMapMarkPointType(MapMarkPointTypeOuterClass.MapMarkPointType pointType) {
|
||||
this.pointType = pointType;
|
||||
}
|
||||
|
||||
public int getMonsterId() {
|
||||
return this.monsterId;
|
||||
}
|
||||
|
||||
public void setMonsterId(int monsterId) {
|
||||
this.monsterId = monsterId;
|
||||
}
|
||||
|
||||
public MapMarkFromTypeOuterClass.MapMarkFromType getMapMarkFromType() {
|
||||
public MapMarkFromType getMapMarkFromType() {
|
||||
return this.fromType;
|
||||
}
|
||||
|
||||
public int getQuestId() {
|
||||
return this.questId;
|
||||
}
|
||||
|
||||
public void setQuestId(int questId) {
|
||||
this.questId = questId;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,61 +1,90 @@
|
||||
package emu.grasscutter.game.managers.MapMarkManager;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.proto.MapMarkPointTypeOuterClass.MapMarkPointType;
|
||||
import emu.grasscutter.net.proto.MarkMapReqOuterClass.MarkMapReq;
|
||||
import emu.grasscutter.net.proto.MarkMapReqOuterClass.MarkMapReq.Operation;
|
||||
import emu.grasscutter.server.packet.send.PacketMarkMapRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify;
|
||||
import emu.grasscutter.utils.Position;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
@Entity
|
||||
public class MapMarksManager {
|
||||
|
||||
static final int mapMarkMaxCount = 150;
|
||||
public static final int mapMarkMaxCount = 150;
|
||||
private HashMap<String, MapMark> mapMarks;
|
||||
private final Player player;
|
||||
|
||||
public MapMarksManager() {
|
||||
mapMarks = new HashMap<String, MapMark>();
|
||||
public MapMarksManager(Player player) {
|
||||
this.player = player;
|
||||
this.mapMarks = player.getMapMarks();
|
||||
if (this.mapMarks == null) { this.mapMarks = new HashMap<>(); }
|
||||
}
|
||||
|
||||
public MapMarksManager(HashMap<String, MapMark> mapMarks) {
|
||||
this.mapMarks = mapMarks;
|
||||
}
|
||||
|
||||
public HashMap<String, MapMark> getAllMapMarks() {
|
||||
return mapMarks;
|
||||
}
|
||||
|
||||
public MapMark getMapMark(Position position) {
|
||||
String key = getMapMarkKey(position);
|
||||
if (mapMarks.containsKey(key)) {
|
||||
return mapMarks.get(key);
|
||||
} else {
|
||||
return null;
|
||||
public void handleMapMarkReq(MarkMapReq req) {
|
||||
Operation op = req.getOp();
|
||||
switch (op) {
|
||||
case ADD -> {
|
||||
MapMark createMark = new MapMark(req.getMark());
|
||||
// keep teleporting functionality on fishhook mark.
|
||||
if (createMark.getMapMarkPointType() == MapMarkPointType.MAP_MARK_POINT_TYPE_FISH_POOL) {
|
||||
teleport(player, createMark);
|
||||
return;
|
||||
}
|
||||
addMapMark(createMark);
|
||||
}
|
||||
case MOD -> {
|
||||
MapMark oldMark = new MapMark(req.getOld());
|
||||
removeMapMark(oldMark.getPosition());
|
||||
MapMark newMark = new MapMark(req.getMark());
|
||||
addMapMark(newMark);
|
||||
}
|
||||
case DEL -> {
|
||||
MapMark deleteMark = new MapMark(req.getMark());
|
||||
removeMapMark(deleteMark.getPosition());
|
||||
}
|
||||
}
|
||||
if (op != Operation.GET) {
|
||||
saveMapMarks();
|
||||
}
|
||||
player.getSession().send(new PacketMarkMapRsp(getMapMarks()));
|
||||
}
|
||||
|
||||
public HashMap<String, MapMark> getMapMarks() {
|
||||
return mapMarks;
|
||||
}
|
||||
|
||||
public String getMapMarkKey(Position position) {
|
||||
return "x" + (int)position.getX()+ "z" + (int)position.getZ();
|
||||
}
|
||||
|
||||
public boolean removeMapMark(Position position) {
|
||||
String key = getMapMarkKey(position);
|
||||
if (mapMarks.containsKey(key)) {
|
||||
mapMarks.remove(key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public void removeMapMark(Position position) {
|
||||
mapMarks.remove(getMapMarkKey(position));
|
||||
}
|
||||
|
||||
public boolean addMapMark(MapMark mapMark) {
|
||||
public void addMapMark(MapMark mapMark) {
|
||||
if (mapMarks.size() < mapMarkMaxCount) {
|
||||
if (!mapMarks.containsKey(getMapMarkKey(mapMark.getPosition()))) {
|
||||
mapMarks.put(getMapMarkKey(mapMark.getPosition()), mapMark);
|
||||
return true;
|
||||
}
|
||||
mapMarks.put(getMapMarkKey(mapMark.getPosition()), mapMark);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setMapMarks(HashMap<String, MapMark> mapMarks) {
|
||||
this.mapMarks = mapMarks;
|
||||
private void saveMapMarks() {
|
||||
player.setMapMarks(mapMarks);
|
||||
player.save();
|
||||
}
|
||||
|
||||
private void teleport(Player player, MapMark mapMark) {
|
||||
float y;
|
||||
try {
|
||||
y = (float)Integer.parseInt(mapMark.getName());
|
||||
} catch (Exception e) {
|
||||
y = 300;
|
||||
}
|
||||
Position pos = mapMark.getPosition();
|
||||
player.getPos().set(pos.getX(), y, pos.getZ());
|
||||
if (mapMark.getSceneId() != player.getSceneId()) {
|
||||
player.getWorld().transferPlayerToScene(player, mapMark.getSceneId(), player.getPos());
|
||||
}
|
||||
player.getScene().broadcastPacket(new PacketSceneEntityAppearNotify(player));
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,14 @@
|
||||
package emu.grasscutter.game.managers;
|
||||
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.entity.EntityAvatar;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass;
|
||||
import emu.grasscutter.net.proto.PropChangeReasonOuterClass;
|
||||
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason;
|
||||
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
|
||||
|
||||
@ -24,7 +22,9 @@ public class SotSManager {
|
||||
// NOTE: Spring volume balance *1 = fight prop HP *100
|
||||
|
||||
private final Player player;
|
||||
private final Logger logger = Grasscutter.getLogger();
|
||||
private Timer autoRecoverTimer;
|
||||
private final boolean enablePriorityHealing = false;
|
||||
|
||||
public final static int GlobalMaximumSpringVolume = 8500000;
|
||||
|
||||
@ -38,6 +38,7 @@ public class SotSManager {
|
||||
|
||||
public void setIsAutoRecoveryEnabled(boolean enabled) {
|
||||
player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, enabled ? 1 : 0);
|
||||
player.save();
|
||||
}
|
||||
|
||||
public int getAutoRecoveryPercentage() {
|
||||
@ -46,49 +47,122 @@ public class SotSManager {
|
||||
|
||||
public void setAutoRecoveryPercentage(int percentage) {
|
||||
player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, percentage);
|
||||
player.save();
|
||||
}
|
||||
|
||||
// autoRevive automatically revives all team members.
|
||||
public void autoRevive(GameSession session) {
|
||||
player.getTeamManager().getActiveTeam().forEach(entity -> {
|
||||
boolean isAlive = entity.isAlive();
|
||||
float currentHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
||||
float maxHP = entity.getAvatar().getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
// Grasscutter.getLogger().debug("" + entity.getAvatar().getAvatarData().getName() + "\t" + currentHP + "/" + maxHP + "\t" + (isAlive ? "ALIVE":"DEAD"));
|
||||
float newHP = (float)(maxHP * 0.3);
|
||||
if (currentHP < newHP) {
|
||||
updateAvatarCurHP(session, entity, newHP);
|
||||
}
|
||||
if (!isAlive) {
|
||||
entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar()));
|
||||
}
|
||||
});
|
||||
public long getLastUsed() {
|
||||
return player.getSpringLastUsed();
|
||||
}
|
||||
|
||||
public void scheduleAutoRecover(GameSession session) {
|
||||
public void setLastUsed() {
|
||||
player.setSpringLastUsed(System.currentTimeMillis() / 1000);
|
||||
player.save();
|
||||
}
|
||||
|
||||
public int getMaxVolume() {
|
||||
return player.getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME);
|
||||
}
|
||||
|
||||
public void setMaxVolume(int volume) {
|
||||
player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, volume);
|
||||
player.save();
|
||||
}
|
||||
|
||||
public int getCurrentVolume() {
|
||||
return player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME);
|
||||
}
|
||||
|
||||
public void setCurrentVolume(int volume) {
|
||||
player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, volume);
|
||||
setLastUsed();
|
||||
player.save();
|
||||
}
|
||||
|
||||
public void handleEnterTransPointRegionNotify() {
|
||||
logger.trace("Player entered statue region");
|
||||
autoRevive();
|
||||
if (autoRecoverTimer == null) {
|
||||
autoRecoverTimer = new Timer();
|
||||
autoRecoverTimer.schedule(new AutoRecoverTimerTick(session), 2500);
|
||||
autoRecoverTimer.schedule(new AutoRecoverTimerTick(), 2500, 15000);
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelAutoRecover() {
|
||||
public void handleExitTransPointRegionNotify() {
|
||||
logger.trace("Player left statue region");
|
||||
if (autoRecoverTimer != null) {
|
||||
autoRecoverTimer.cancel();
|
||||
autoRecoverTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class AutoRecoverTimerTick extends TimerTask
|
||||
{
|
||||
private GameSession session;
|
||||
// autoRevive automatically revives all team members.
|
||||
public void autoRevive() {
|
||||
player.getTeamManager().getActiveTeam().forEach(entity -> {
|
||||
boolean isAlive = entity.isAlive();
|
||||
if (isAlive) {
|
||||
return;
|
||||
}
|
||||
logger.trace("Reviving avatar " + entity.getAvatar().getAvatarData().getName());
|
||||
player.getTeamManager().reviveAvatar(entity.getAvatar());
|
||||
player.getTeamManager().healAvatar(entity.getAvatar(), 30, 0);
|
||||
});
|
||||
}
|
||||
|
||||
public AutoRecoverTimerTick(GameSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
private class AutoRecoverTimerTick extends TimerTask {
|
||||
// autoRecover checks player setting to see if auto recover is enabled, and refill HP to the predefined level.
|
||||
public void run() {
|
||||
autoRecover(session);
|
||||
cancelAutoRecover();
|
||||
refillSpringVolume();
|
||||
|
||||
logger.trace("isAutoRecoveryEnabled: " + getIsAutoRecoveryEnabled() + "\tautoRecoverPercentage: " + getAutoRecoveryPercentage());
|
||||
|
||||
if (getIsAutoRecoveryEnabled()) {
|
||||
List<EntityAvatar> activeTeam = player.getTeamManager().getActiveTeam();
|
||||
// When the statue does not have enough remaining volume:
|
||||
// Enhanced experience: Enable priority healing
|
||||
// The current active character will get healed first, then sequential.
|
||||
// Vanilla experience: Disable priority healing
|
||||
// Sequential healing based on character index.
|
||||
int priorityIndex = enablePriorityHealing ? player.getTeamManager().getCurrentCharacterIndex() : -1;
|
||||
if (priorityIndex >= 0) {
|
||||
checkAndHealAvatar(activeTeam.get(priorityIndex));
|
||||
}
|
||||
for (int i = 0; i < activeTeam.size(); i++) {
|
||||
if (i != priorityIndex) {
|
||||
checkAndHealAvatar(activeTeam.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void checkAndHealAvatar(EntityAvatar entity) {
|
||||
int maxHP = (int) (entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * 100);
|
||||
int currentHP = (int) (entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) * 100);
|
||||
if (currentHP == maxHP) {
|
||||
return;
|
||||
}
|
||||
int targetHP = maxHP * getAutoRecoveryPercentage() / 100;
|
||||
|
||||
if (targetHP > currentHP) {
|
||||
int needHP = targetHP - currentHP;
|
||||
int currentVolume = getCurrentVolume();
|
||||
if (currentVolume >= needHP) {
|
||||
// sufficient
|
||||
setCurrentVolume(currentVolume - needHP);
|
||||
} else {
|
||||
// insufficient balance
|
||||
needHP = currentVolume;
|
||||
setCurrentVolume(0);
|
||||
}
|
||||
if (needHP > 0) {
|
||||
logger.trace("Healing avatar " + entity.getAvatar().getAvatarData().getName() + " +" + needHP);
|
||||
player.getTeamManager().healAvatar(entity.getAvatar(), 0, needHP);
|
||||
player.getSession().send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP,
|
||||
((float) needHP / 100), List.of(3), PropChangeReason.PROP_CHANGE_STATUE_RECOVER,
|
||||
ChangeHpReason.ChangeHpAddStatue));
|
||||
player.getSession().send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,84 +170,23 @@ public class SotSManager {
|
||||
// Temporary: Max spring volume depends on level of the statues in Mondstadt and Liyue. Override until we have statue level.
|
||||
// TODO: remove
|
||||
// https://genshin-impact.fandom.com/wiki/Statue_of_The_Seven#:~:text=region%20of%20Inazuma.-,Statue%20Levels,-Upon%20first%20unlocking
|
||||
player.setProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME, 8500000);
|
||||
setMaxVolume(8500000);
|
||||
// Temporary: Auto enable 100% statue recovery until we can adjust statue settings in game
|
||||
// TODO: remove
|
||||
player.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, 100);
|
||||
player.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1);
|
||||
setAutoRecoveryPercentage(100);
|
||||
setIsAutoRecoveryEnabled(true);
|
||||
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
long secondsSinceLastUsed = now - player.getSpringLastUsed();
|
||||
float percentageRefilled = (float)secondsSinceLastUsed / 15 / 100; // 15s = 1% max volume
|
||||
int maxVolume = player.getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME);
|
||||
int currentVolume = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME);
|
||||
int maxVolume = getMaxVolume();
|
||||
int currentVolume = getCurrentVolume();
|
||||
if (currentVolume < maxVolume) {
|
||||
int volumeRefilled = (int)(percentageRefilled * maxVolume);
|
||||
int newVolume = currentVolume + volumeRefilled;
|
||||
if (currentVolume + volumeRefilled > maxVolume) {
|
||||
newVolume = maxVolume;
|
||||
}
|
||||
player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, newVolume);
|
||||
}
|
||||
player.setSpringLastUsed(now);
|
||||
player.save();
|
||||
}
|
||||
|
||||
// autoRecover checks player setting to see if auto recover is enabled, and refill HP to the predefined level.
|
||||
public void autoRecover(GameSession session) {
|
||||
// TODO: In MP, respect SotS settings from the HOST.
|
||||
boolean isAutoRecoveryEnabled = getIsAutoRecoveryEnabled();
|
||||
int autoRecoverPercentage = getAutoRecoveryPercentage();
|
||||
Grasscutter.getLogger().debug("isAutoRecoveryEnabled: " + isAutoRecoveryEnabled + "\tautoRecoverPercentage: " + autoRecoverPercentage);
|
||||
|
||||
if (isAutoRecoveryEnabled) {
|
||||
player.getTeamManager().getActiveTeam().forEach(entity -> {
|
||||
float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
||||
if (currentHP == maxHP) {
|
||||
return;
|
||||
}
|
||||
float targetHP = maxHP * autoRecoverPercentage / 100;
|
||||
|
||||
if (targetHP > currentHP) {
|
||||
float needHP = targetHP - currentHP;
|
||||
float needSV = needHP * 100; // convert HP needed to Spring Volume needed
|
||||
|
||||
int sotsSVBalance = player.getProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME);
|
||||
if (sotsSVBalance >= needSV) {
|
||||
// sufficient
|
||||
sotsSVBalance -= needSV;
|
||||
} else {
|
||||
// insufficient balance
|
||||
needSV = sotsSVBalance;
|
||||
sotsSVBalance = 0;
|
||||
}
|
||||
player.setProperty(PlayerProperty.PROP_CUR_SPRING_VOLUME, sotsSVBalance);
|
||||
player.setSpringLastUsed(System.currentTimeMillis() / 1000);
|
||||
|
||||
float newHP = currentHP + needSV / 100; // convert SV to HP
|
||||
|
||||
updateAvatarCurHP(session, entity, newHP);
|
||||
}
|
||||
});
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
int secondsSinceLastUsed = (int) (now - getLastUsed());
|
||||
// 15s = 1% max volume
|
||||
int volumeRefilled = secondsSinceLastUsed * maxVolume / 15 / 100;
|
||||
logger.trace("Statue has refilled HP volume: " + volumeRefilled);
|
||||
currentVolume = Math.min(currentVolume + volumeRefilled, maxVolume);
|
||||
logger.trace("Statue remaining HP volume: " + currentVolume);
|
||||
setCurrentVolume(currentVolume);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAvatarCurHP(GameSession session, EntityAvatar entity, float newHP) {
|
||||
// TODO: Figure out why client shows current HP instead of added HP.
|
||||
// Say an avatar had 12000 and now has 14000, it should show "2000".
|
||||
// The client always show "+14000" which is incorrect.
|
||||
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
|
||||
session.send(new PacketEntityFightPropChangeReasonNotify(entity, FightProperty.FIGHT_PROP_CUR_HP,
|
||||
newHP, List.of(3), PropChangeReasonOuterClass.PropChangeReason.PROP_CHANGE_STATUE_RECOVER,
|
||||
ChangeHpReasonOuterClass.ChangeHpReason.ChangeHpAddStatue));
|
||||
session.send(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
|
||||
Avatar avatar = entity.getAvatar();
|
||||
avatar.setCurrentHp(newHP);
|
||||
session.send(new PacketAvatarFightPropUpdateNotify(avatar, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
player.save();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,5 +8,5 @@ public interface AfterUpdateStaminaListener {
|
||||
* @param reason Why updating stamina.
|
||||
* @param newStamina New Stamina value.
|
||||
*/
|
||||
void onAfterUpdateStamina(String reason, int newStamina);
|
||||
void onAfterUpdateStamina(String reason, int newStamina, boolean isCharacterStamina);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ public interface BeforeUpdateStaminaListener {
|
||||
* @param newStamina New ABSOLUTE stamina value.
|
||||
* @return true if you want to cancel this update, otherwise false.
|
||||
*/
|
||||
int onBeforeUpdateStamina(String reason, int newStamina);
|
||||
int onBeforeUpdateStamina(String reason, int newStamina, boolean isCharacterStamina);
|
||||
/**
|
||||
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
|
||||
* This gives listeners a chance to intercept this update.
|
||||
@ -16,5 +16,5 @@ public interface BeforeUpdateStaminaListener {
|
||||
* @param consumption ConsumptionType and RELATIVE stamina change amount.
|
||||
* @return true if you want to cancel this update, otherwise false.
|
||||
*/
|
||||
Consumption onBeforeUpdateStamina(String reason, Consumption consumption);
|
||||
Consumption onBeforeUpdateStamina(String reason, Consumption consumption, boolean isCharacterStamina);
|
||||
}
|
@ -13,18 +13,19 @@ public enum ConsumptionType {
|
||||
// Slow swimming is handled per movement, not per second.
|
||||
// Arm movement frequency depends on gender/age/height.
|
||||
// TODO: Instead of cost -80 per tick, find a proper way to calculate cost.
|
||||
SKIFF(-300), // TODO: Get real value
|
||||
SKIFF_DASH(-204),
|
||||
SPRINT(-1800),
|
||||
SWIM_DASH_START(-20),
|
||||
SWIM_DASH_START(-2000),
|
||||
SWIM_DASH(-204), // -10.2 per second, 5Hz = -204 each tick
|
||||
SWIMMING(-80),
|
||||
TALENT_DASH(-300), // -1500 per second, 5Hz = -300 each tick
|
||||
TALENT_DASH_START(-1000),
|
||||
|
||||
// restore
|
||||
POWERED_FLY(500), // TODO: Get real value
|
||||
POWERED_SKIFF(2000), // TODO: Get real value
|
||||
POWERED_FLY(500),
|
||||
POWERED_SKIFF(500),
|
||||
RUN(500),
|
||||
SKIFF(500),
|
||||
STANDBY(500),
|
||||
WALK(500);
|
||||
|
||||
|
@ -2,6 +2,7 @@ package emu.grasscutter.game.managers.StaminaManager;
|
||||
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.game.entity.EntityAvatar;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
@ -13,21 +14,21 @@ import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
|
||||
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
|
||||
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
|
||||
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
|
||||
import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.Math;
|
||||
import java.util.*;
|
||||
|
||||
import static emu.grasscutter.Configuration.*;
|
||||
import static emu.grasscutter.Configuration.GAME_OPTIONS;
|
||||
|
||||
public class StaminaManager {
|
||||
|
||||
// TODO: Skiff state detection?
|
||||
private final Player player;
|
||||
private final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{
|
||||
private static final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{
|
||||
put("CLIMB", new HashSet<>(List.of(
|
||||
MotionState.MOTION_CLIMB, // sustained, when not moving no cost no recover
|
||||
MotionState.MOTION_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY
|
||||
@ -48,7 +49,7 @@ public class StaminaManager {
|
||||
)));
|
||||
put("SKIFF", new HashSet<>(List.of(
|
||||
MotionState.MOTION_SKIFF_BOARDING, // NOT OBSERVED even when boarding
|
||||
MotionState.MOTION_SKIFF_DASH, // NOT OBSERVED even when dashing
|
||||
MotionState.MOTION_SKIFF_DASH, // sustained, observed with waverider entity ID.
|
||||
MotionState.MOTION_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing
|
||||
MotionState.MOTION_SKIFF_POWERED_DASH // sustained, recover
|
||||
)));
|
||||
@ -108,7 +109,8 @@ public class StaminaManager {
|
||||
}};
|
||||
|
||||
private final Logger logger = Grasscutter.getLogger();
|
||||
public final static int GlobalMaximumStamina = 24000;
|
||||
public final static int GlobalCharacterMaximumStamina = 24000;
|
||||
public final static int GlobalVehicleMaxStamina = 24000;
|
||||
private Position currentCoordinates = new Position(0, 0, 0);
|
||||
private Position previousCoordinates = new Position(0, 0, 0);
|
||||
private MotionState currentState = MotionState.MOTION_STANDBY;
|
||||
@ -122,74 +124,58 @@ public class StaminaManager {
|
||||
private int lastSkillId = 0;
|
||||
private int lastSkillCasterId = 0;
|
||||
private boolean lastSkillFirstTick = true;
|
||||
public static final HashSet<Integer> TalentMovements = new HashSet<>(List.of(
|
||||
10013, // Kamisato Ayaka
|
||||
10413 // Mona
|
||||
private int vehicleId = -1;
|
||||
private int vehicleStamina = GlobalVehicleMaxStamina;
|
||||
private static final HashSet<Integer> TalentMovements = new HashSet<>(List.of(
|
||||
10013, 10413
|
||||
));
|
||||
private static final HashMap<Integer, Float> ClimbFoodReductionMap = new HashMap<>() {{
|
||||
// TODO: get real food id
|
||||
put(0, 0.8f); // Sample food
|
||||
}};
|
||||
private static final HashMap<Integer, Float> DashFoodReductionMap = new HashMap<>() {{
|
||||
// TODO: get real food id
|
||||
put(0, 0.8f); // Sample food
|
||||
}};
|
||||
private static final HashMap<Integer, Float> FlyFoodReductionMap = new HashMap<>() {{
|
||||
// TODO: get real food id
|
||||
put(0, 0.8f); // Sample food
|
||||
}};
|
||||
private static final HashMap<Integer, Float> SwimFoodReductionMap = new HashMap<>() {{
|
||||
// TODO: get real food id
|
||||
put(0, 0.8f); // Sample food
|
||||
}};
|
||||
private static final HashMap<Integer, Float> ClimbTalentReductionMap = new HashMap<>() {{
|
||||
put(262301, 0.8f);
|
||||
}};
|
||||
private static final HashMap<Integer, Float> FlyTalentReductionMap = new HashMap<>() {{
|
||||
put(212301, 0.8f);
|
||||
put(222301, 0.8f);
|
||||
}};
|
||||
private static final HashMap<Integer, Float> SwimTalentReductionMap = new HashMap<>() {{
|
||||
put(242301, 0.8f);
|
||||
put(542301, 0.8f);
|
||||
}};
|
||||
|
||||
// TODO: Get from somewhere else, instead of hard-coded here?
|
||||
public static final HashSet<Integer> ClaymoreSkills = new HashSet<>(List.of(
|
||||
10160, // Diluc, /=2
|
||||
10201, // Razor
|
||||
10241, // Beidou
|
||||
10341, // Noelle
|
||||
10401, // Chongyun
|
||||
10441, // Xinyan
|
||||
10511, // Eula
|
||||
10531, // Sayu
|
||||
10571 // Arataki Itto, = 0
|
||||
));
|
||||
public static final HashSet<Integer> CatalystSkills = new HashSet<>(List.of(
|
||||
10060, // Lisa
|
||||
10070, // Barbara
|
||||
10271, // Ningguang
|
||||
10291, // Klee
|
||||
10411, // Mona
|
||||
10431, // Sucrose
|
||||
10481, // Yanfei
|
||||
10541, // Sangonomoiya Kokomi
|
||||
10581 // Yae Miko
|
||||
));
|
||||
public static final HashSet<Integer> PolearmSkills = new HashSet<>(List.of(
|
||||
10231, // Xiangling
|
||||
10261, // Xiao
|
||||
10301, // Zhongli
|
||||
10451, // Rosaria
|
||||
10461, // Hu Tao
|
||||
10501, // Thoma
|
||||
10521, // Raiden Shogun
|
||||
10631, // Shenhe
|
||||
10641 // Yunjin
|
||||
));
|
||||
public static final HashSet<Integer> SwordSkills = new HashSet<>(List.of(
|
||||
10024, // Kamisato Ayaka
|
||||
10031, // Jean
|
||||
10073, // Kaeya
|
||||
10321, // Bennett
|
||||
10337, // Tartaglia, melee stance (10332 switch to melee, 10336 switch to ranged stance)
|
||||
10351, // Qiqi
|
||||
10381, // Xingqiu
|
||||
10386, // Albedo
|
||||
10421, // Keqing, =-2500
|
||||
10471, // Kaedehara Kazuha
|
||||
10661, // Kamisato Ayato
|
||||
100553, // Lumine
|
||||
100540 // Aether
|
||||
));
|
||||
public static final HashSet<Integer> BowSkills = new HashSet<>(List.of(
|
||||
10041, 10043, // Amber
|
||||
10221, 10223,// Venti
|
||||
10311, 10315, // Fischl
|
||||
10331, 10335, // Tartaglia, ranged stance
|
||||
10371, // Ganyu
|
||||
10391, 10394, // Diona
|
||||
10491, // Yoimiya
|
||||
10551, 10554, // Gorou
|
||||
10561, 10564, // Kojou Sara
|
||||
10621, // Aloy
|
||||
99998, 99999 // Yelan // TODO: get real values
|
||||
));
|
||||
public static final HashSet<Integer> BowAvatars = new HashSet<>();
|
||||
public static final HashSet<Integer> CatalystAvatars = new HashSet<>();
|
||||
public static final HashSet<Integer> ClaymoreAvatars = new HashSet<>();
|
||||
public static final HashSet<Integer> PolearmAvatars = new HashSet<>();
|
||||
public static final HashSet<Integer> SwordAvatars = new HashSet<>();
|
||||
|
||||
public static void initialize() {
|
||||
// Initialize skill categories
|
||||
GameData.getAvatarDataMap().forEach((avatarId, avatarData) -> {
|
||||
switch (avatarData.getWeaponType()) {
|
||||
case "WEAPON_BOW" -> BowAvatars.add(avatarId);
|
||||
case "WEAPON_CLAYMORE" -> ClaymoreAvatars.add(avatarId);
|
||||
case "WEAPON_CATALYST" -> CatalystAvatars.add(avatarId);
|
||||
case "WEAPON_POLE" -> PolearmAvatars.add(avatarId);
|
||||
case "WEAPON_SWORD_ONE_HAND" -> SwordAvatars.add(avatarId);
|
||||
}
|
||||
});
|
||||
// TODO: Initialize foods etc.
|
||||
}
|
||||
|
||||
public StaminaManager(Player player) {
|
||||
this.player = player;
|
||||
@ -203,6 +189,22 @@ public class StaminaManager {
|
||||
lastSkillCasterId = skillCasterId;
|
||||
}
|
||||
|
||||
public int getMaxCharacterStamina() {
|
||||
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
}
|
||||
|
||||
public int getCurrentCharacterStamina() {
|
||||
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
}
|
||||
|
||||
public int getMaxVehicleStamina() {
|
||||
return GlobalVehicleMaxStamina;
|
||||
}
|
||||
|
||||
public int getCurrentVehicleStamina() {
|
||||
return vehicleStamina;
|
||||
}
|
||||
|
||||
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
|
||||
if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
|
||||
return false;
|
||||
@ -244,67 +246,71 @@ public class StaminaManager {
|
||||
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
|
||||
}
|
||||
|
||||
public int updateStaminaRelative(GameSession session, Consumption consumption) {
|
||||
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
public int updateStaminaRelative(GameSession session, Consumption consumption, boolean isCharacterStamina) {
|
||||
int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
|
||||
if (consumption.amount == 0) {
|
||||
return currentStamina;
|
||||
}
|
||||
// notify will update
|
||||
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
|
||||
Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption);
|
||||
Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption, isCharacterStamina);
|
||||
if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) {
|
||||
logger.debug("[StaminaManager] Stamina update relative(" +
|
||||
logger.debug("Stamina update relative(" +
|
||||
consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" +
|
||||
consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey());
|
||||
return currentStamina;
|
||||
}
|
||||
}
|
||||
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
logger.trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
|
||||
int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
|
||||
logger.trace((isCharacterStamina ? "C " : "V ") + currentStamina + "/" + maxStamina + "\t" + currentState + "\t" +
|
||||
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," +
|
||||
consumption.amount + ")");
|
||||
int newStamina = currentStamina + consumption.amount;
|
||||
if (newStamina < 0) {
|
||||
newStamina = 0;
|
||||
} else if (newStamina > playerMaxStamina) {
|
||||
newStamina = playerMaxStamina;
|
||||
} else if (newStamina > maxStamina) {
|
||||
newStamina = maxStamina;
|
||||
}
|
||||
return setStamina(session, consumption.type.toString(), newStamina);
|
||||
return setStamina(session, consumption.type.toString(), newStamina, isCharacterStamina);
|
||||
}
|
||||
|
||||
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) {
|
||||
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
|
||||
int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
|
||||
// notify will update
|
||||
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
|
||||
int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina);
|
||||
int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina, isCharacterStamina);
|
||||
if (overriddenNewStamina != newStamina) {
|
||||
logger.debug("[StaminaManager] Stamina update absolute(" +
|
||||
logger.debug("Stamina update absolute(" +
|
||||
reason + ", " + newStamina + ") overridden to absolute(" +
|
||||
reason + ", " + newStamina + ") by: " + listener.getKey());
|
||||
return currentStamina;
|
||||
}
|
||||
}
|
||||
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
|
||||
if (newStamina < 0) {
|
||||
newStamina = 0;
|
||||
} else if (newStamina > playerMaxStamina) {
|
||||
newStamina = playerMaxStamina;
|
||||
} else if (newStamina > maxStamina) {
|
||||
newStamina = maxStamina;
|
||||
}
|
||||
return setStamina(session, reason, newStamina);
|
||||
return setStamina(session, reason, newStamina, isCharacterStamina);
|
||||
}
|
||||
|
||||
// Returns new stamina and sends PlayerPropNotify
|
||||
public int setStamina(GameSession session, String reason, int newStamina) {
|
||||
// Returns new stamina and sends PlayerPropNotify or VehicleStaminaNotify
|
||||
public int setStamina(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
|
||||
if (!GAME_OPTIONS.staminaUsage) {
|
||||
newStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
newStamina = getMaxCharacterStamina();
|
||||
}
|
||||
// set stamina if is character stamina
|
||||
if (isCharacterStamina) {
|
||||
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
|
||||
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
|
||||
} else {
|
||||
vehicleStamina = newStamina;
|
||||
session.send(new PacketVehicleStaminaNotify(vehicleId, ((float) newStamina) / 100));
|
||||
}
|
||||
|
||||
// set stamina
|
||||
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
|
||||
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
|
||||
// notify updated
|
||||
for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) {
|
||||
listener.getValue().onAfterUpdateStamina(reason, newStamina);
|
||||
listener.getValue().onAfterUpdateStamina(reason, newStamina, isCharacterStamina);
|
||||
}
|
||||
return newStamina;
|
||||
}
|
||||
@ -343,22 +349,23 @@ public class StaminaManager {
|
||||
// External trigger handler
|
||||
|
||||
public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) {
|
||||
// Ignore if skill not cast by not current active
|
||||
// Ignore if skill not cast by not current active avatar
|
||||
if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) {
|
||||
return;
|
||||
}
|
||||
setSkillCast(skillId, casterId);
|
||||
// Handle immediate stamina cost
|
||||
if (ClaymoreSkills.contains(skillId)) {
|
||||
int currentAvatarId = player.getTeamManager().getCurrentAvatarEntity().getAvatar().getAvatarId();
|
||||
if (ClaymoreAvatars.contains(currentAvatarId)) {
|
||||
// Exclude claymore as their stamina cost starts when MixinStaminaCost gets in
|
||||
return;
|
||||
}
|
||||
// TODO: Differentiate normal attacks from charged attacks and exclude
|
||||
// TODO: Temporary: Exclude non-claymore attacks for now
|
||||
if (BowSkills.contains(skillId)
|
||||
|| SwordSkills.contains(skillId)
|
||||
|| PolearmSkills.contains(skillId)
|
||||
|| CatalystSkills.contains(skillId)
|
||||
if (BowAvatars.contains(currentAvatarId)
|
||||
|| SwordAvatars.contains(currentAvatarId)
|
||||
|| PolearmAvatars.contains(currentAvatarId)
|
||||
|| CatalystAvatars.contains(currentAvatarId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -367,7 +374,7 @@ public class StaminaManager {
|
||||
|
||||
public void handleMixinCostStamina(boolean isSwim) {
|
||||
// Talent moving and claymore avatar charged attack duration
|
||||
// logger.trace("abilityMixinCostStamina: isSwim: " + isSwim);
|
||||
// logger.trace("abilityMixinCostStamina: isSwim: " + isSwim + "\tlastSkill: " + lastSkillId);
|
||||
if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) {
|
||||
handleImmediateStamina(cachedSession, lastSkillId);
|
||||
}
|
||||
@ -381,11 +388,11 @@ public class StaminaManager {
|
||||
MotionState motionState = motionInfo.getState();
|
||||
int notifyEntityId = entity.getId();
|
||||
int currentAvatarEntityId = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getId();
|
||||
if (notifyEntityId != currentAvatarEntityId) {
|
||||
if (notifyEntityId != currentAvatarEntityId && notifyEntityId != vehicleId) {
|
||||
return;
|
||||
}
|
||||
currentState = motionState;
|
||||
// logger.trace("" + currentState);
|
||||
// logger.trace(currentState + "\t" + (notifyEntityId == currentAvatarEntityId ? "character" : "vehicle"));
|
||||
Vector posVector = motionInfo.getPos();
|
||||
Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ());
|
||||
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
|
||||
@ -395,28 +402,40 @@ public class StaminaManager {
|
||||
handleImmediateStamina(session, motionState);
|
||||
}
|
||||
|
||||
public void handleVehicleInteractReq(GameSession session, int vehicleId, VehicleInteractType vehicleInteractType) {
|
||||
if (vehicleInteractType == VehicleInteractType.VEHICLE_INTERACT_IN) {
|
||||
this.vehicleId = vehicleId;
|
||||
// Reset character stamina here to prevent falling into water immediately on ejection if char stamina is
|
||||
// close to empty when boarding.
|
||||
updateStaminaAbsolute(session, "board vehicle", getMaxCharacterStamina(), true);
|
||||
updateStaminaAbsolute(session, "board vehicle", getMaxVehicleStamina(), false);
|
||||
} else {
|
||||
this.vehicleId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Internal handler
|
||||
|
||||
private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) {
|
||||
switch (motionState) {
|
||||
case MOTION_CLIMB:
|
||||
if (currentState != MotionState.MOTION_CLIMB) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START));
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START), true);
|
||||
}
|
||||
break;
|
||||
case MOTION_DASH_BEFORE_SHAKE:
|
||||
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT));
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT), true);
|
||||
}
|
||||
break;
|
||||
case MOTION_CLIMB_JUMP:
|
||||
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP));
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP), true);
|
||||
}
|
||||
break;
|
||||
case MOTION_SWIM_DASH:
|
||||
if (previousState != MotionState.MOTION_SWIM_DASH) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START));
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START), true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -424,18 +443,20 @@ public class StaminaManager {
|
||||
|
||||
private void handleImmediateStamina(GameSession session, int skillId) {
|
||||
Consumption consumption = getFightConsumption(skillId);
|
||||
updateStaminaRelative(session, consumption);
|
||||
updateStaminaRelative(session, consumption, true);
|
||||
}
|
||||
|
||||
private class SustainedStaminaHandler extends TimerTask {
|
||||
public void run() {
|
||||
boolean moving = isPlayerMoving();
|
||||
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
if (moving || (currentStamina < maxStamina)) {
|
||||
int currentCharacterStamina = getCurrentCharacterStamina();
|
||||
int maxCharacterStamina = getMaxCharacterStamina();
|
||||
int currentVehicleStamina = getCurrentVehicleStamina();
|
||||
int maxVehicleStamina = getMaxVehicleStamina();
|
||||
if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) {
|
||||
logger.trace("Player moving: " + moving + ", stamina full: " +
|
||||
(currentStamina >= maxStamina) + ", recalculate stamina");
|
||||
|
||||
(currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina");
|
||||
boolean isCharacterStamina = true;
|
||||
Consumption consumption;
|
||||
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
|
||||
consumption = getClimbConsumption();
|
||||
@ -447,43 +468,44 @@ public class StaminaManager {
|
||||
consumption = new Consumption(ConsumptionType.RUN);
|
||||
} else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) {
|
||||
consumption = getSkiffConsumption();
|
||||
isCharacterStamina = false;
|
||||
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.STANDBY);
|
||||
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
|
||||
} else if (MotionStatesCategorized.get("SWIM").contains(currentState)) {
|
||||
consumption = getSwimConsumptions();
|
||||
} else if (MotionStatesCategorized.get("WALK").contains((currentState))) {
|
||||
} else if (MotionStatesCategorized.get("WALK").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.WALK);
|
||||
} else if (MotionStatesCategorized.get("OTHER").contains((currentState))) {
|
||||
} else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) {
|
||||
consumption = new Consumption();
|
||||
} else if (MotionStatesCategorized.get("OTHER").contains(currentState)) {
|
||||
consumption = getOtherConsumptions();
|
||||
} else {
|
||||
// ignore
|
||||
} else { // ignore
|
||||
return;
|
||||
}
|
||||
if (consumption.amount < 0) {
|
||||
/* Do not apply reduction factor when recovering stamina
|
||||
TODO: Reductions that apply to all motion types:
|
||||
Elemental Resonance
|
||||
Wind: -15%
|
||||
Skills
|
||||
Diona E: -10% while shield lasts - applies to SP+MP
|
||||
Barbara E: -12% while lasts - applies to SP+MP
|
||||
*/
|
||||
|
||||
if (consumption.amount < 0 && isCharacterStamina) {
|
||||
// Do not apply reduction factor when recovering stamina
|
||||
if (player.getTeamManager().getTeamResonances().contains(10301)) {
|
||||
consumption.amount *= 0.85f;
|
||||
}
|
||||
}
|
||||
// Delay 2 seconds before starts recovering stamina
|
||||
if (cachedSession != null) {
|
||||
// Delay 1 seconds before starts recovering stamina
|
||||
if (consumption.amount != 0 && cachedSession != null) {
|
||||
if (consumption.amount < 0) {
|
||||
staminaRecoverDelay = 0;
|
||||
}
|
||||
if (consumption.amount > 0 && consumption.type != ConsumptionType.POWERED_FLY) {
|
||||
// For POWERED_FLY recover immediately - things like Amber's gliding exam may require this.
|
||||
if (staminaRecoverDelay < 10) {
|
||||
// For others recover after 2 seconds (10 ticks) - as official server does.
|
||||
if (consumption.amount > 0
|
||||
&& consumption.type != ConsumptionType.POWERED_FLY
|
||||
&& consumption.type != ConsumptionType.POWERED_SKIFF) {
|
||||
// For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this.
|
||||
if (staminaRecoverDelay < 5) {
|
||||
// For others recover after 1 seconds (5 ticks) - as official server does.
|
||||
staminaRecoverDelay++;
|
||||
consumption.amount = 0;
|
||||
logger.trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay);
|
||||
logger.trace("Delaying recovery: " + staminaRecoverDelay);
|
||||
}
|
||||
}
|
||||
updateStaminaRelative(cachedSession, consumption);
|
||||
updateStaminaRelative(cachedSession, consumption, isCharacterStamina);
|
||||
}
|
||||
}
|
||||
previousState = currentState;
|
||||
@ -496,10 +518,11 @@ public class StaminaManager {
|
||||
}
|
||||
|
||||
private void handleDrowning() {
|
||||
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
// TODO: fix drowning waverider entity
|
||||
int stamina = getCurrentCharacterStamina();
|
||||
if (stamina < 10) {
|
||||
logger.trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" +
|
||||
player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState);
|
||||
logger.trace(getCurrentCharacterStamina() + "/" +
|
||||
getMaxCharacterStamina() + "\t" + currentState);
|
||||
if (currentState != MotionState.MOTION_SWIM_IDLE) {
|
||||
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
|
||||
}
|
||||
@ -517,24 +540,25 @@ public class StaminaManager {
|
||||
return getTalentMovingSustainedCost(skillCasting);
|
||||
}
|
||||
// Bow avatar charged attack
|
||||
if (BowSkills.contains(skillCasting)) {
|
||||
int currentAvatarId = player.getTeamManager().getCurrentAvatarEntity().getAvatar().getAvatarId();
|
||||
if (BowAvatars.contains(currentAvatarId)) {
|
||||
return getBowSustainedCost(skillCasting);
|
||||
}
|
||||
// Claymore avatar charged attack
|
||||
if (ClaymoreSkills.contains(skillCasting)) {
|
||||
if (ClaymoreAvatars.contains(currentAvatarId)) {
|
||||
return getClaymoreSustainedCost(skillCasting);
|
||||
}
|
||||
// Catalyst avatar charged attack
|
||||
if (CatalystSkills.contains(skillCasting)) {
|
||||
return getCatalystSustainedCost(skillCasting);
|
||||
if (CatalystAvatars.contains(currentAvatarId)) {
|
||||
return getCatalystCost(skillCasting);
|
||||
}
|
||||
// Polearm avatar charged attack
|
||||
if (PolearmSkills.contains(skillCasting)) {
|
||||
return getPolearmSustainedCost(skillCasting);
|
||||
if (PolearmAvatars.contains(currentAvatarId)) {
|
||||
return getPolearmCost(skillCasting);
|
||||
}
|
||||
// Sword avatar charged attack
|
||||
if (SwordSkills.contains(skillCasting)) {
|
||||
return getSwordSustainedCost(skillCasting);
|
||||
if (SwordAvatars.contains(skillCasting)) {
|
||||
return getSwordCost(skillCasting);
|
||||
}
|
||||
return new Consumption();
|
||||
}
|
||||
@ -546,18 +570,8 @@ public class StaminaManager {
|
||||
consumption.amount = ConsumptionType.CLIMBING.amount;
|
||||
}
|
||||
// Climbing specific reductions
|
||||
// TODO: create a food cost reduction map
|
||||
HashMap<Integer, Float> foodReductionMap = new HashMap<>() {{
|
||||
// TODO: get real talent id
|
||||
put(0, 0.8f); // Sample food
|
||||
}};
|
||||
consumption.amount *= getFoodCostReductionFactor(foodReductionMap);
|
||||
|
||||
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
|
||||
// TODO: get real talent id
|
||||
put(0, 0.8f); // Xiao
|
||||
}};
|
||||
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
|
||||
consumption.amount *= getFoodCostReductionFactor(ClimbFoodReductionMap);
|
||||
consumption.amount *= getTalentCostReductionFactor(ClimbTalentReductionMap);
|
||||
return consumption;
|
||||
}
|
||||
|
||||
@ -572,13 +586,9 @@ public class StaminaManager {
|
||||
consumption.type = ConsumptionType.SWIM_DASH;
|
||||
consumption.amount = ConsumptionType.SWIM_DASH.amount;
|
||||
}
|
||||
// Reductions
|
||||
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
|
||||
// TODO: get real talent id
|
||||
put(0, 0.8f); // Beidou
|
||||
put(1, 0.8f); // Sangonomiya Kokomi
|
||||
}};
|
||||
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
|
||||
// Swimming specific reductions
|
||||
consumption.amount *= getFoodCostReductionFactor(SwimFoodReductionMap);
|
||||
consumption.amount *= getTalentCostReductionFactor(SwimTalentReductionMap);
|
||||
return consumption;
|
||||
}
|
||||
|
||||
@ -587,8 +597,8 @@ public class StaminaManager {
|
||||
if (currentState == MotionState.MOTION_DASH) {
|
||||
consumption.type = ConsumptionType.DASH;
|
||||
consumption.amount = ConsumptionType.DASH.amount;
|
||||
// TODO: Dashing specific reductions
|
||||
// Foods:
|
||||
// Dashing specific reductions
|
||||
consumption.amount *= getFoodCostReductionFactor(DashFoodReductionMap);
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
@ -599,32 +609,34 @@ public class StaminaManager {
|
||||
return new Consumption(ConsumptionType.POWERED_FLY);
|
||||
}
|
||||
Consumption consumption = new Consumption(ConsumptionType.FLY);
|
||||
// Passive Talents
|
||||
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
|
||||
put(212301, 0.8f); // Amber
|
||||
put(222301, 0.8f); // Venti
|
||||
}};
|
||||
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
|
||||
// TODO: Foods
|
||||
// Flying specific reductions
|
||||
consumption.amount *= getFoodCostReductionFactor(FlyFoodReductionMap);
|
||||
consumption.amount *= getTalentCostReductionFactor(FlyTalentReductionMap);
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getSkiffConsumption() {
|
||||
// POWERED_SKIFF, e.g. wind tunnel
|
||||
if (currentState == MotionState.MOTION_SKIFF_POWERED_DASH) {
|
||||
return new Consumption(ConsumptionType.POWERED_SKIFF);
|
||||
}
|
||||
// No known reduction for skiffing.
|
||||
return new Consumption(ConsumptionType.SKIFF);
|
||||
return switch (currentState) {
|
||||
case MOTION_SKIFF_DASH -> new Consumption(ConsumptionType.SKIFF_DASH);
|
||||
case MOTION_SKIFF_POWERED_DASH -> new Consumption(ConsumptionType.POWERED_SKIFF);
|
||||
case MOTION_SKIFF_NORMAL -> new Consumption(ConsumptionType.SKIFF);
|
||||
default -> new Consumption();
|
||||
};
|
||||
}
|
||||
|
||||
private Consumption getOtherConsumptions() {
|
||||
if (currentState == MotionState.MOTION_NOTIFY) {
|
||||
if (BowSkills.contains(lastSkillId)) {
|
||||
switch (currentState) {
|
||||
case MOTION_NOTIFY:
|
||||
// if (BowSkills.contains(lastSkillId)) {
|
||||
// return new Consumption(ConsumptionType.FIGHT, 500);
|
||||
// }
|
||||
break;
|
||||
case MOTION_FIGHT:
|
||||
// TODO: what if charged attack
|
||||
return new Consumption(ConsumptionType.FIGHT, 500);
|
||||
}
|
||||
}
|
||||
// TODO: Add other logic
|
||||
|
||||
return new Consumption();
|
||||
}
|
||||
|
||||
@ -671,11 +683,11 @@ public class StaminaManager {
|
||||
return new Consumption(ConsumptionType.FIGHT, +500);
|
||||
}
|
||||
|
||||
private Consumption getCatalystSustainedCost(int skillId) {
|
||||
private Consumption getCatalystCost(int skillId) {
|
||||
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -5000);
|
||||
// Character specific handling
|
||||
switch (skillId) {
|
||||
// TODO: Yanfei
|
||||
// TODO:
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
@ -684,18 +696,20 @@ public class StaminaManager {
|
||||
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -1333); // 4000 / 3 = 1333
|
||||
// Character specific handling
|
||||
switch (skillId) {
|
||||
case 10571: // Arataki Itto, does not consume stamina at all.
|
||||
case 10571:
|
||||
case 10532:
|
||||
consumption.amount = 0;
|
||||
break;
|
||||
case 10160: // Diluc, with talent "Relentless" stamina cost is decreased by 50%
|
||||
// TODO: How to get talent status?
|
||||
consumption.amount /= 2;
|
||||
case 10160:
|
||||
if (player.getTeamManager().getCurrentAvatarEntity().getAvatar().getProudSkillList().contains(162101)) {
|
||||
consumption.amount /= 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getPolearmSustainedCost(int skillId) {
|
||||
private Consumption getPolearmCost(int skillId) {
|
||||
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2500);
|
||||
// Character specific handling
|
||||
switch (skillId) {
|
||||
@ -704,11 +718,11 @@ public class StaminaManager {
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getSwordSustainedCost(int skillId) {
|
||||
private Consumption getSwordCost(int skillId) {
|
||||
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000);
|
||||
// Character specific handling
|
||||
switch (skillId) {
|
||||
case 10421: // Keqing, -2500
|
||||
case 10421:
|
||||
consumption.amount = -2500;
|
||||
break;
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.game.props.EntityType;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
import emu.grasscutter.game.props.SceneType;
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.QuestManager;
|
||||
import emu.grasscutter.game.shop.ShopLimit;
|
||||
import emu.grasscutter.game.managers.MapMarkManager.*;
|
||||
import emu.grasscutter.game.tower.TowerManager;
|
||||
@ -82,6 +85,11 @@ public class Player {
|
||||
private Set<Integer> flyCloakList;
|
||||
private Set<Integer> costumeList;
|
||||
|
||||
private Integer widgetId;
|
||||
|
||||
private Set<Integer> realmList;
|
||||
private Integer currentRealmId;
|
||||
|
||||
@Transient private long nextGuid = 0;
|
||||
@Transient private int peerId;
|
||||
@Transient private World world;
|
||||
@ -93,6 +101,7 @@ public class Player {
|
||||
@Transient private MailHandler mailHandler;
|
||||
@Transient private MessageHandler messageHandler;
|
||||
@Transient private AbilityManager abilityManager;
|
||||
@Transient private QuestManager questManager;
|
||||
|
||||
@Transient private SotSManager sotsManager;
|
||||
|
||||
@ -132,10 +141,11 @@ public class Player {
|
||||
@Transient private final InvokeHandler<AbilityInvokeEntry> abilityInvokeHandler;
|
||||
@Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
|
||||
|
||||
private MapMarksManager mapMarksManager;
|
||||
@Transient private MapMarksManager mapMarksManager;
|
||||
@Transient private StaminaManager staminaManager;
|
||||
|
||||
private long springLastUsed;
|
||||
private HashMap<String, MapMark> mapMarks;
|
||||
|
||||
|
||||
@Deprecated
|
||||
@ -147,6 +157,7 @@ public class Player {
|
||||
this.mailHandler = new MailHandler(this);
|
||||
this.towerManager = new TowerManager(this);
|
||||
this.abilityManager = new AbilityManager(this);
|
||||
this.setQuestManager(new QuestManager(this));
|
||||
this.pos = new Position();
|
||||
this.rotation = new Position();
|
||||
this.properties = new HashMap<>();
|
||||
@ -179,7 +190,7 @@ public class Player {
|
||||
this.shopLimit = new ArrayList<>();
|
||||
this.expeditionInfo = new HashMap<>();
|
||||
this.messageHandler = null;
|
||||
this.mapMarksManager = new MapMarksManager();
|
||||
this.mapMarksManager = new MapMarksManager(this);
|
||||
this.staminaManager = new StaminaManager(this);
|
||||
this.sotsManager = new SotSManager(this);
|
||||
}
|
||||
@ -207,7 +218,7 @@ public class Player {
|
||||
this.getPos().set(GameConstants.START_POSITION);
|
||||
this.getRotation().set(0, 307, 0);
|
||||
this.messageHandler = null;
|
||||
this.mapMarksManager = new MapMarksManager();
|
||||
this.mapMarksManager = new MapMarksManager(this);
|
||||
this.staminaManager = new StaminaManager(this);
|
||||
this.sotsManager = new SotSManager(this);
|
||||
}
|
||||
@ -297,6 +308,39 @@ public class Player {
|
||||
this.updateProfile();
|
||||
}
|
||||
|
||||
public Integer getWidgetId() {
|
||||
return widgetId;
|
||||
}
|
||||
|
||||
public void setWidgetId(Integer widgetId) {
|
||||
this.widgetId = widgetId;
|
||||
}
|
||||
|
||||
public Set<Integer> getRealmList() {
|
||||
return realmList;
|
||||
}
|
||||
|
||||
public void setRealmList(Set<Integer> realmList) {
|
||||
this.realmList = realmList;
|
||||
}
|
||||
|
||||
public void addRealmList(int realmId) {
|
||||
if (this.realmList == null) {
|
||||
this.realmList = new HashSet<>();
|
||||
} else if (this.realmList.contains(realmId)) {
|
||||
return;
|
||||
}
|
||||
this.realmList.add(realmId);
|
||||
}
|
||||
|
||||
public Integer getCurrentRealmId() {
|
||||
return currentRealmId;
|
||||
}
|
||||
|
||||
public void setCurrentRealmId(Integer currentRealmId) {
|
||||
this.currentRealmId = currentRealmId;
|
||||
}
|
||||
|
||||
public Position getPos() {
|
||||
return pos;
|
||||
}
|
||||
@ -411,6 +455,14 @@ public class Player {
|
||||
return towerManager;
|
||||
}
|
||||
|
||||
public QuestManager getQuestManager() {
|
||||
return questManager;
|
||||
}
|
||||
|
||||
public void setQuestManager(QuestManager questManager) {
|
||||
this.questManager = questManager;
|
||||
}
|
||||
|
||||
public PlayerGachaInfo getGachaInfo() {
|
||||
return gachaInfo;
|
||||
}
|
||||
@ -885,9 +937,7 @@ public class Player {
|
||||
}
|
||||
|
||||
public void sendPacket(BasePacket packet) {
|
||||
if (this.hasSentAvatarDataNotify) {
|
||||
this.getSession().send(packet);
|
||||
}
|
||||
this.getSession().send(packet);
|
||||
}
|
||||
|
||||
public OnlinePlayerInfo getOnlinePlayerInfo() {
|
||||
@ -1034,6 +1084,10 @@ public class Player {
|
||||
return abilityManager;
|
||||
}
|
||||
|
||||
public HashMap<String, MapMark> getMapMarks() { return mapMarks; }
|
||||
|
||||
public void setMapMarks(HashMap<String, MapMark> newMarks) { mapMarks = newMarks; }
|
||||
|
||||
public synchronized void onTick() {
|
||||
// Check ping
|
||||
if (this.getLastPingTime() > System.currentTimeMillis() + 60000) {
|
||||
@ -1120,7 +1174,23 @@ public class Player {
|
||||
|
||||
this.getFriendsList().loadFromDatabase();
|
||||
this.getMailHandler().loadFromDatabase();
|
||||
this.getQuestManager().loadFromDatabase();
|
||||
|
||||
// Quest - Commented out because a problem is caused if you log out while this quest is active
|
||||
/*
|
||||
if (getQuestManager().getMainQuestById(351) == null) {
|
||||
GameQuest quest = getQuestManager().addQuest(35104);
|
||||
if (quest != null) {
|
||||
quest.finish();
|
||||
}
|
||||
|
||||
getQuestManager().addQuest(35101);
|
||||
|
||||
this.setSceneId(3);
|
||||
this.getPos().set(GameConstants.START_POSITION);
|
||||
}
|
||||
*/
|
||||
|
||||
// Create world
|
||||
World world = new World(this);
|
||||
world.addPlayer(this);
|
||||
@ -1140,6 +1210,14 @@ public class Player {
|
||||
session.send(new PacketStoreWeightLimitNotify());
|
||||
session.send(new PacketPlayerStoreNotify(this));
|
||||
session.send(new PacketAvatarDataNotify(this));
|
||||
session.send(new PacketFinishedParentQuestNotify(this));
|
||||
session.send(new PacketQuestListNotify(this));
|
||||
session.send(new PacketCodexDataFullNotify(this));
|
||||
session.send(new PacketServerCondMeetQuestListUpdateNotify(this));
|
||||
session.send(new PacketAllWidgetDataNotify(this));
|
||||
session.send(new PacketWidgetGadgetAllDataNotify());
|
||||
session.send(new PacketPlayerHomeCompInfoNotify(this));
|
||||
session.send(new PacketHomeComfortInfoNotify(this));
|
||||
|
||||
getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward.
|
||||
|
||||
@ -1237,7 +1315,7 @@ public class Player {
|
||||
} else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009
|
||||
if (!(0 <= value && value <= 1)) { return false; }
|
||||
} else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010
|
||||
if (!(value >= 0 && value <= StaminaManager.GlobalMaximumStamina)) { return false; }
|
||||
if (!(value >= 0 && value <= StaminaManager.GlobalCharacterMaximumStamina)) { return false; }
|
||||
} else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011
|
||||
int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
if (!(value >= 0 && value <= playerMaximumStamina)) { return false; }
|
||||
|
126
src/main/java/emu/grasscutter/game/quest/GameMainQuest.java
Normal file
126
src/main/java/emu/grasscutter/game/quest/GameMainQuest.java
Normal file
@ -0,0 +1,126 @@
|
||||
package emu.grasscutter.game.quest;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Id;
|
||||
import dev.morphia.annotations.Indexed;
|
||||
import dev.morphia.annotations.Transient;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.enums.ParentQuestState;
|
||||
import emu.grasscutter.game.quest.enums.QuestState;
|
||||
import emu.grasscutter.net.proto.ChildQuestOuterClass.ChildQuest;
|
||||
import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest;
|
||||
import emu.grasscutter.net.proto.QuestOuterClass.Quest;
|
||||
import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
@Entity(value = "quests", useDiscriminator = false)
|
||||
public class GameMainQuest {
|
||||
@Id private ObjectId id;
|
||||
|
||||
@Indexed private int ownerUid;
|
||||
@Transient private Player owner;
|
||||
|
||||
private Map<Integer, GameQuest> childQuests;
|
||||
|
||||
private int parentQuestId;
|
||||
private int[] questVars;
|
||||
private ParentQuestState state;
|
||||
private boolean isFinished;
|
||||
|
||||
@Deprecated // Morphia only. Do not use.
|
||||
public GameMainQuest() {}
|
||||
|
||||
public GameMainQuest(Player player, int parentQuestId) {
|
||||
this.owner = player;
|
||||
this.ownerUid = player.getUid();
|
||||
this.parentQuestId = parentQuestId;
|
||||
this.childQuests = new HashMap<>();
|
||||
this.questVars = new int[5];
|
||||
this.state = ParentQuestState.PARENT_QUEST_STATE_NONE;
|
||||
}
|
||||
|
||||
public int getParentQuestId() {
|
||||
return parentQuestId;
|
||||
}
|
||||
|
||||
public int getOwnerUid() {
|
||||
return ownerUid;
|
||||
}
|
||||
|
||||
public Player getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(Player player) {
|
||||
if (player.getUid() != this.getOwnerUid()) return;
|
||||
this.owner = player;
|
||||
}
|
||||
|
||||
public Map<Integer, GameQuest> getChildQuests() {
|
||||
return childQuests;
|
||||
}
|
||||
|
||||
public GameQuest getChildQuestById(int id) {
|
||||
return this.getChildQuests().get(id);
|
||||
}
|
||||
|
||||
public int[] getQuestVars() {
|
||||
return questVars;
|
||||
}
|
||||
|
||||
public ParentQuestState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return isFinished;
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
this.isFinished = true;
|
||||
this.state = ParentQuestState.PARENT_QUEST_STATE_FINISHED;
|
||||
this.getOwner().getSession().send(new PacketFinishedParentQuestUpdateNotify(this));
|
||||
this.getOwner().getSession().send(new PacketCodexDataUpdateNotify(this));
|
||||
this.save();
|
||||
}
|
||||
|
||||
public void save() {
|
||||
DatabaseHelper.saveQuest(this);
|
||||
}
|
||||
|
||||
public ParentQuest toProto() {
|
||||
ParentQuest.Builder proto = ParentQuest.newBuilder()
|
||||
.setParentQuestId(getParentQuestId())
|
||||
.setIsFinished(isFinished())
|
||||
.setParentQuestState(getState().getValue());
|
||||
|
||||
for (GameQuest quest : this.getChildQuests().values()) {
|
||||
ChildQuest childQuest = ChildQuest.newBuilder()
|
||||
.setQuestId(quest.getQuestId())
|
||||
.setState(quest.getState().getValue())
|
||||
.build();
|
||||
|
||||
proto.addChildQuestList(childQuest);
|
||||
}
|
||||
|
||||
if (getQuestVars() != null) {
|
||||
for (int i : getQuestVars()) {
|
||||
proto.addQuestVar(i);
|
||||
}
|
||||
}
|
||||
|
||||
return proto.build();
|
||||
}
|
||||
}
|
223
src/main/java/emu/grasscutter/game/quest/GameQuest.java
Normal file
223
src/main/java/emu/grasscutter/game/quest/GameQuest.java
Normal file
@ -0,0 +1,223 @@
|
||||
package emu.grasscutter.game.quest;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Transient;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.custom.MainQuestData;
|
||||
import emu.grasscutter.data.custom.MainQuestData.SubQuestData;
|
||||
import emu.grasscutter.data.def.QuestData;
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.enums.LogicType;
|
||||
import emu.grasscutter.game.quest.enums.QuestState;
|
||||
import emu.grasscutter.net.proto.QuestOuterClass.Quest;
|
||||
import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
@Entity
|
||||
public class GameQuest {
|
||||
@Transient private GameMainQuest mainQuest;
|
||||
@Transient private QuestData questData;
|
||||
|
||||
private int questId;
|
||||
private int mainQuestId;
|
||||
private QuestState state;
|
||||
|
||||
private int startTime;
|
||||
private int acceptTime;
|
||||
private int finishTime;
|
||||
|
||||
private int[] finishProgressList;
|
||||
private int[] failProgressList;
|
||||
|
||||
@Deprecated // Morphia only. Do not use.
|
||||
public GameQuest() {}
|
||||
|
||||
public GameQuest(GameMainQuest mainQuest, QuestData questData) {
|
||||
this.mainQuest = mainQuest;
|
||||
this.questId = questData.getId();
|
||||
this.mainQuestId = questData.getMainId();
|
||||
this.questData = questData;
|
||||
this.acceptTime = Utils.getCurrentSeconds();
|
||||
this.startTime = this.acceptTime;
|
||||
this.state = QuestState.QUEST_STATE_UNFINISHED;
|
||||
|
||||
if (questData.getFinishCond()!= null) {
|
||||
this.finishProgressList = new int[questData.getFinishCond().length];
|
||||
}
|
||||
|
||||
if (questData.getFailCond() != null) {
|
||||
this.failProgressList = new int[questData.getFailCond().length];
|
||||
}
|
||||
|
||||
this.mainQuest.getChildQuests().put(this.questId, this);
|
||||
}
|
||||
|
||||
public GameMainQuest getMainQuest() {
|
||||
return mainQuest;
|
||||
}
|
||||
|
||||
public void setMainQuest(GameMainQuest mainQuest) {
|
||||
this.mainQuest = mainQuest;
|
||||
}
|
||||
|
||||
public Player getOwner() {
|
||||
return getMainQuest().getOwner();
|
||||
}
|
||||
|
||||
public int getQuestId() {
|
||||
return questId;
|
||||
}
|
||||
|
||||
public int getMainQuestId() {
|
||||
return mainQuestId;
|
||||
}
|
||||
|
||||
public QuestData getData() {
|
||||
return questData;
|
||||
}
|
||||
|
||||
public void setConfig(QuestData config) {
|
||||
if (this.getQuestId() != config.getId()) return;
|
||||
this.questData = config;
|
||||
}
|
||||
|
||||
public QuestState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(QuestState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public int getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(int startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public int getAcceptTime() {
|
||||
return acceptTime;
|
||||
}
|
||||
|
||||
public void setAcceptTime(int acceptTime) {
|
||||
this.acceptTime = acceptTime;
|
||||
}
|
||||
|
||||
public int getFinishTime() {
|
||||
return finishTime;
|
||||
}
|
||||
|
||||
public void setFinishTime(int finishTime) {
|
||||
this.finishTime = finishTime;
|
||||
}
|
||||
|
||||
public int[] getFinishProgressList() {
|
||||
return finishProgressList;
|
||||
}
|
||||
|
||||
public void setFinishProgress(int index, int value) {
|
||||
finishProgressList[index] = value;
|
||||
}
|
||||
|
||||
public int[] getFailProgressList() {
|
||||
return failProgressList;
|
||||
}
|
||||
|
||||
public void setFailProgress(int index, int value) {
|
||||
failProgressList[index] = value;
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
this.state = QuestState.QUEST_STATE_FINISHED;
|
||||
this.finishTime = Utils.getCurrentSeconds();
|
||||
|
||||
if (this.getFinishProgressList() != null) {
|
||||
for (int i = 0 ; i < getFinishProgressList().length; i++) {
|
||||
getFinishProgressList()[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.getOwner().getSession().send(new PacketQuestProgressUpdateNotify(this));
|
||||
this.getOwner().getSession().send(new PacketQuestListUpdateNotify(this));
|
||||
|
||||
if (this.getData().finishParent()) {
|
||||
// This quest finishes the questline - the main quest will also save the quest to db so we dont have to call save() here
|
||||
this.getMainQuest().finish();
|
||||
} else {
|
||||
// Try and accept other quests if possible
|
||||
this.tryAcceptQuestLine();
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean tryAcceptQuestLine() {
|
||||
try {
|
||||
MainQuestData questConfig = GameData.getMainQuestDataMap().get(this.getMainQuestId());
|
||||
|
||||
for (SubQuestData subQuest : questConfig.getSubQuests()) {
|
||||
GameQuest quest = getMainQuest().getChildQuestById(subQuest.getSubId());
|
||||
|
||||
if (quest == null) {
|
||||
QuestData questData = GameData.getQuestDataMap().get(subQuest.getSubId());
|
||||
|
||||
if (questData == null || questData.getAcceptCond() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int[] accept = new int[questData.getAcceptCond().length];
|
||||
|
||||
// TODO
|
||||
for (int i = 0; i < questData.getAcceptCond().length; i++) {
|
||||
QuestCondition condition = questData.getAcceptCond()[i];
|
||||
boolean result = getOwner().getServer().getQuestHandler().triggerCondition(this, condition, condition.getParam());
|
||||
|
||||
accept[i] = result ? 1 : 0;
|
||||
}
|
||||
|
||||
boolean shouldAccept = LogicType.calculate(questData.getAcceptCondComb(), accept);
|
||||
|
||||
if (shouldAccept) {
|
||||
this.getOwner().getQuestManager().addQuest(questData.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
getMainQuest().save();
|
||||
}
|
||||
|
||||
public Quest toProto() {
|
||||
Quest.Builder proto = Quest.newBuilder()
|
||||
.setQuestId(this.getQuestId())
|
||||
.setState(this.getState().getValue())
|
||||
.setParentQuestId(this.getMainQuestId())
|
||||
.setStartTime(this.getStartTime())
|
||||
.setStartGameTime(438)
|
||||
.setAcceptTime(this.getAcceptTime());
|
||||
|
||||
if (this.getFinishProgressList() != null) {
|
||||
for (int i : this.getFinishProgressList()) {
|
||||
proto.addFinishProgressList(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getFailProgressList() != null) {
|
||||
for (int i : this.getFailProgressList()) {
|
||||
proto.addFailProgressList(i);
|
||||
}
|
||||
}
|
||||
|
||||
return proto.build();
|
||||
}
|
||||
}
|
188
src/main/java/emu/grasscutter/game/quest/QuestManager.java
Normal file
188
src/main/java/emu/grasscutter/game/quest/QuestManager.java
Normal file
@ -0,0 +1,188 @@
|
||||
package emu.grasscutter.game.quest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.def.QuestData;
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.enums.LogicType;
|
||||
import emu.grasscutter.game.quest.enums.QuestState;
|
||||
import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketServerCondMeetQuestListUpdateNotify;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
public class QuestManager {
|
||||
private final Player player;
|
||||
private final Int2ObjectMap<GameMainQuest> quests;
|
||||
|
||||
public QuestManager(Player player) {
|
||||
this.player = player;
|
||||
this.quests = new Int2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public Int2ObjectMap<GameMainQuest> getQuests() {
|
||||
return quests;
|
||||
}
|
||||
|
||||
public GameMainQuest getMainQuestById(int mainQuestId) {
|
||||
return getQuests().get(mainQuestId);
|
||||
}
|
||||
|
||||
public GameQuest getQuestById(int questId) {
|
||||
QuestData questConfig = GameData.getQuestDataMap().get(questId);
|
||||
if (questConfig == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GameMainQuest mainQuest = getQuests().get(questConfig.getMainId());
|
||||
|
||||
if (mainQuest == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mainQuest.getChildQuests().get(questId);
|
||||
}
|
||||
|
||||
public void forEachQuest(Consumer<GameQuest> callback) {
|
||||
for (GameMainQuest mainQuest : getQuests().values()) {
|
||||
for (GameQuest quest : mainQuest.getChildQuests().values()) {
|
||||
callback.accept(quest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void forEachMainQuest(Consumer<GameMainQuest> callback) {
|
||||
for (GameMainQuest mainQuest : getQuests().values()) {
|
||||
callback.accept(mainQuest);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
public void forEachActiveQuest(Consumer<GameQuest> callback) {
|
||||
for (GameMainQuest mainQuest : getQuests().values()) {
|
||||
for (GameQuest quest : mainQuest.getChildQuests().values()) {
|
||||
if (quest.getState() != QuestState.QUEST_STATE_FINISHED) {
|
||||
callback.accept(quest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GameMainQuest addMainQuest(QuestData questConfig) {
|
||||
GameMainQuest mainQuest = new GameMainQuest(getPlayer(), questConfig.getMainId());
|
||||
getQuests().put(mainQuest.getParentQuestId(), mainQuest);
|
||||
|
||||
getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(mainQuest));
|
||||
|
||||
return mainQuest;
|
||||
}
|
||||
|
||||
public GameQuest addQuest(int questId) {
|
||||
QuestData questConfig = GameData.getQuestDataMap().get(questId);
|
||||
if (questConfig == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Main quest
|
||||
GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainId());
|
||||
|
||||
// Create main quest if it doesnt exist
|
||||
if (mainQuest == null) {
|
||||
mainQuest = addMainQuest(questConfig);
|
||||
}
|
||||
|
||||
// Sub quest
|
||||
GameQuest quest = mainQuest.getChildQuestById(questId);
|
||||
|
||||
if (quest != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create
|
||||
quest = new GameQuest(mainQuest, questConfig);
|
||||
|
||||
// Save main quest
|
||||
mainQuest.save();
|
||||
|
||||
// Send packet
|
||||
getPlayer().sendPacket(new PacketServerCondMeetQuestListUpdateNotify(quest));
|
||||
getPlayer().sendPacket(new PacketQuestListUpdateNotify(quest));
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
public void triggerEvent(QuestTrigger condType, int... params) {
|
||||
Set<GameQuest> changedQuests = new HashSet<>();
|
||||
|
||||
this.forEachActiveQuest(quest -> {
|
||||
QuestData data = quest.getData();
|
||||
|
||||
for (int i = 0; i < data.getFinishCond().length; i++) {
|
||||
if (quest.getFinishProgressList() == null || quest.getFinishProgressList()[i] == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QuestCondition condition = data.getFinishCond()[i];
|
||||
|
||||
if (condition.getType() != condType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean result = getPlayer().getServer().getQuestHandler().triggerContent(quest, condition, params);
|
||||
|
||||
if (result) {
|
||||
quest.getFinishProgressList()[i] = 1;
|
||||
|
||||
changedQuests.add(quest);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (GameQuest quest : changedQuests) {
|
||||
LogicType logicType = quest.getData().getFailCondComb();
|
||||
int[] progress = quest.getFinishProgressList();
|
||||
|
||||
// Handle logical comb
|
||||
boolean finish = LogicType.calculate(logicType, progress);
|
||||
|
||||
// Finish
|
||||
if (finish) {
|
||||
quest.finish();
|
||||
} else {
|
||||
getPlayer().sendPacket(new PacketQuestProgressUpdateNotify(quest));
|
||||
quest.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadFromDatabase() {
|
||||
List<GameMainQuest> quests = DatabaseHelper.getAllQuests(getPlayer());
|
||||
|
||||
for (GameMainQuest mainQuest : quests) {
|
||||
mainQuest.setOwner(this.getPlayer());
|
||||
|
||||
for (GameQuest quest : mainQuest.getChildQuests().values()) {
|
||||
quest.setMainQuest(mainQuest);
|
||||
quest.setConfig(GameData.getQuestDataMap().get(quest.getQuestId()));
|
||||
}
|
||||
|
||||
this.getQuests().put(mainQuest.getParentQuestId(), mainQuest);
|
||||
}
|
||||
}
|
||||
}
|
11
src/main/java/emu/grasscutter/game/quest/QuestValue.java
Normal file
11
src/main/java/emu/grasscutter/game/quest/QuestValue.java
Normal file
@ -0,0 +1,11 @@
|
||||
package emu.grasscutter.game.quest;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface QuestValue {
|
||||
QuestTrigger value();
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package emu.grasscutter.game.quest;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class ServerQuestHandler {
|
||||
private final Int2ObjectMap<QuestBaseHandler> condHandlers;
|
||||
private final Int2ObjectMap<QuestBaseHandler> contHandlers;
|
||||
private final Int2ObjectMap<QuestBaseHandler> execHandlers;
|
||||
|
||||
public ServerQuestHandler() {
|
||||
this.condHandlers = new Int2ObjectOpenHashMap<>();
|
||||
this.contHandlers = new Int2ObjectOpenHashMap<>();
|
||||
this.execHandlers = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
public void registerHandlers() {
|
||||
this.registerHandlers(this.condHandlers, "emu.grasscutter.game.quest.conditions");
|
||||
this.registerHandlers(this.contHandlers, "emu.grasscutter.game.quest.content");
|
||||
this.registerHandlers(this.execHandlers, "emu.grasscutter.game.quest.exec");
|
||||
}
|
||||
|
||||
public void registerHandlers(Int2ObjectMap<QuestBaseHandler> map, String packageName) {
|
||||
Reflections reflections = new Reflections(packageName);
|
||||
Set<?> handlerClasses = reflections.getSubTypesOf(QuestBaseHandler.class);
|
||||
|
||||
for (Object obj : handlerClasses) {
|
||||
this.registerPacketHandler(map, (Class<? extends QuestBaseHandler>) obj);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerPacketHandler(Int2ObjectMap<QuestBaseHandler> map, Class<? extends QuestBaseHandler> handlerClass) {
|
||||
try {
|
||||
QuestValue opcode = handlerClass.getAnnotation(QuestValue.class);
|
||||
|
||||
if (opcode == null || opcode.value().getValue() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QuestBaseHandler packetHandler = (QuestBaseHandler) handlerClass.newInstance();
|
||||
|
||||
map.put(opcode.value().getValue(), packetHandler);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make cleaner
|
||||
|
||||
public boolean triggerCondition(GameQuest quest, QuestCondition condition, int... params) {
|
||||
QuestBaseHandler handler = condHandlers.get(condition.getType().getValue());
|
||||
|
||||
if (handler == null || quest.getData() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return handler.execute(quest, condition, params);
|
||||
}
|
||||
|
||||
public boolean triggerContent(GameQuest quest, QuestCondition condition, int... params) {
|
||||
QuestBaseHandler handler = contHandlers.get(condition.getType().getValue());
|
||||
|
||||
if (handler == null || quest.getData() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return handler.execute(quest, condition, params);
|
||||
}
|
||||
|
||||
public boolean triggerExec(GameQuest quest, QuestCondition condition, int... params) {
|
||||
QuestBaseHandler handler = execHandlers.get(condition.getType().getValue());
|
||||
|
||||
if (handler == null || quest.getData() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return handler.execute(quest, condition, params);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package emu.grasscutter.game.quest.conditions;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.QuestValue;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
|
||||
@QuestValue(QuestTrigger.QUEST_CONTENT_NONE)
|
||||
public class BaseCondition extends QuestBaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
|
||||
// TODO Auto-generated method stub
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package emu.grasscutter.game.quest.conditions;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.QuestValue;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
|
||||
@QuestValue(QuestTrigger.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER)
|
||||
public class ConditionPlayerLevelEqualGreater extends QuestBaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
|
||||
return quest.getOwner().getLevel() >= params[0];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package emu.grasscutter.game.quest.conditions;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.QuestValue;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
|
||||
@QuestValue(QuestTrigger.QUEST_COND_STATE_EQUAL)
|
||||
public class ConditionStateEqual extends QuestBaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
|
||||
GameQuest checkQuest = quest.getOwner().getQuestManager().getQuestById(params[0]);
|
||||
|
||||
if (checkQuest != null) {
|
||||
return checkQuest.getState().getValue() == params[1];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package emu.grasscutter.game.quest.content;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.QuestValue;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
|
||||
@QuestValue(QuestTrigger.QUEST_CONTENT_NONE)
|
||||
public class BaseContent extends QuestBaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
|
||||
// TODO Auto-generated method stub
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package emu.grasscutter.game.quest.content;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.QuestValue;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
|
||||
@QuestValue(QuestTrigger.QUEST_CONTENT_COMPLETE_TALK)
|
||||
public class ContentCompleteTalk extends QuestBaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
|
||||
return condition.getParam()[0] == params[0];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package emu.grasscutter.game.quest.content;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.QuestValue;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
|
||||
@QuestValue(QuestTrigger.QUEST_CONTENT_ENTER_DUNGEON)
|
||||
public class ContentEnterDungeon extends QuestBaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
|
||||
return condition.getParam()[0] == params[0];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package emu.grasscutter.game.quest.content;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.QuestValue;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
|
||||
@QuestValue(QuestTrigger.QUEST_CONTENT_FINISH_PLOT)
|
||||
public class ContentFinishPlot extends QuestBaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
|
||||
return condition.getParam()[0] == params[0];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum LogicType {
|
||||
LOGIC_NONE (0),
|
||||
LOGIC_AND (1),
|
||||
LOGIC_OR (2),
|
||||
LOGIC_NOT (3),
|
||||
LOGIC_A_AND_ETCOR (4),
|
||||
LOGIC_A_AND_B_AND_ETCOR (5),
|
||||
LOGIC_A_OR_ETCAND (6),
|
||||
LOGIC_A_OR_B_OR_ETCAND (7),
|
||||
LOGIC_A_AND_B_OR_ETCAND (8);
|
||||
|
||||
private final int value;
|
||||
|
||||
LogicType(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static boolean calculate(LogicType logicType, int[] progress) {
|
||||
if (logicType == null) {
|
||||
return progress[0] == 1;
|
||||
}
|
||||
|
||||
switch (logicType) {
|
||||
case LOGIC_AND -> {
|
||||
return Arrays.stream(progress).allMatch(i -> i == 1);
|
||||
}
|
||||
case LOGIC_OR -> {
|
||||
return Arrays.stream(progress).anyMatch(i -> i == 1);
|
||||
}
|
||||
default -> {
|
||||
return Arrays.stream(progress).anyMatch(i -> i == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
public enum ParentQuestState {
|
||||
PARENT_QUEST_STATE_NONE (0),
|
||||
PARENT_QUEST_STATE_FINISHED (1),
|
||||
PARENT_QUEST_STATE_FAILED (2),
|
||||
PARENT_QUEST_STATE_CANCELED (3);
|
||||
|
||||
private final int value;
|
||||
|
||||
ParentQuestState(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
public enum QuestGuideType {
|
||||
QUEST_GUIDE_NONE (0),
|
||||
QUEST_GUIDE_LOCATION (1),
|
||||
QUEST_GUIDE_NPC (2);
|
||||
|
||||
private final int value;
|
||||
|
||||
QuestGuideType(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
public enum QuestShowType {
|
||||
QUEST_SHOW (0),
|
||||
QUEST_HIDDEN (1);
|
||||
|
||||
private final int value;
|
||||
|
||||
QuestShowType(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
public enum QuestState {
|
||||
QUEST_STATE_NONE (0),
|
||||
QUEST_STATE_UNSTARTED (1),
|
||||
QUEST_STATE_UNFINISHED (2),
|
||||
QUEST_STATE_FINISHED (3),
|
||||
QUEST_STATE_FAILED (4);
|
||||
|
||||
private final int value;
|
||||
|
||||
QuestState(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
235
src/main/java/emu/grasscutter/game/quest/enums/QuestTrigger.java
Normal file
235
src/main/java/emu/grasscutter/game/quest/enums/QuestTrigger.java
Normal file
@ -0,0 +1,235 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
public enum QuestTrigger {
|
||||
QUEST_COND_NONE (0),
|
||||
QUEST_COND_STATE_EQUAL (1),
|
||||
QUEST_COND_STATE_NOT_EQUAL (2),
|
||||
QUEST_COND_PACK_HAVE_ITEM (3),
|
||||
QUEST_COND_AVATAR_ELEMENT_EQUAL (4),
|
||||
QUEST_COND_AVATAR_ELEMENT_NOT_EQUAL (5),
|
||||
QUEST_COND_AVATAR_CAN_CHANGE_ELEMENT (6),
|
||||
QUEST_COND_CITY_LEVEL_EQUAL_GREATER (7),
|
||||
QUEST_COND_ITEM_NUM_LESS_THAN (8),
|
||||
QUEST_COND_DAILY_TASK_START (9),
|
||||
QUEST_COND_OPEN_STATE_EQUAL (10),
|
||||
QUEST_COND_DAILY_TASK_OPEN (11),
|
||||
QUEST_COND_DAILY_TASK_REWARD_CAN_GET (12),
|
||||
QUEST_COND_DAILY_TASK_REWARD_RECEIVED (13),
|
||||
QUEST_COND_PLAYER_LEVEL_REWARD_CAN_GET (14),
|
||||
QUEST_COND_EXPLORATION_REWARD_CAN_GET (15),
|
||||
QUEST_COND_IS_WORLD_OWNER (16),
|
||||
QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER (17),
|
||||
QUEST_COND_SCENE_AREA_UNLOCKED (18),
|
||||
QUEST_COND_ITEM_GIVING_ACTIVED (19),
|
||||
QUEST_COND_ITEM_GIVING_FINISHED (20),
|
||||
QUEST_COND_IS_DAYTIME (21),
|
||||
QUEST_COND_CURRENT_AVATAR (22),
|
||||
QUEST_COND_CURRENT_AREA (23),
|
||||
QUEST_COND_QUEST_VAR_EQUAL (24),
|
||||
QUEST_COND_QUEST_VAR_GREATER (25),
|
||||
QUEST_COND_QUEST_VAR_LESS (26),
|
||||
QUEST_COND_FORGE_HAVE_FINISH (27),
|
||||
QUEST_COND_DAILY_TASK_IN_PROGRESS (28),
|
||||
QUEST_COND_DAILY_TASK_FINISHED (29),
|
||||
QUEST_COND_ACTIVITY_COND (30),
|
||||
QUEST_COND_ACTIVITY_OPEN (31),
|
||||
QUEST_COND_DAILY_TASK_VAR_GT (32),
|
||||
QUEST_COND_DAILY_TASK_VAR_EQ (33),
|
||||
QUEST_COND_DAILY_TASK_VAR_LT (34),
|
||||
QUEST_COND_BARGAIN_ITEM_GT (35),
|
||||
QUEST_COND_BARGAIN_ITEM_EQ (36),
|
||||
QUEST_COND_BARGAIN_ITEM_LT (37),
|
||||
QUEST_COND_COMPLETE_TALK (38),
|
||||
QUEST_COND_NOT_HAVE_BLOSSOM_TALK (39),
|
||||
QUEST_COND_IS_CUR_BLOSSOM_TALK (40),
|
||||
QUEST_COND_QUEST_NOT_RECEIVE (41),
|
||||
QUEST_COND_QUEST_SERVER_COND_VALID (42),
|
||||
QUEST_COND_ACTIVITY_CLIENT_COND (43),
|
||||
QUEST_COND_QUEST_GLOBAL_VAR_EQUAL (44),
|
||||
QUEST_COND_QUEST_GLOBAL_VAR_GREATER (45),
|
||||
QUEST_COND_QUEST_GLOBAL_VAR_LESS (46),
|
||||
QUEST_COND_PERSONAL_LINE_UNLOCK (47),
|
||||
QUEST_COND_CITY_REPUTATION_REQUEST (48),
|
||||
QUEST_COND_MAIN_COOP_START (49),
|
||||
QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT (50),
|
||||
QUEST_COND_CITY_REPUTATION_LEVEL (51),
|
||||
QUEST_COND_CITY_REPUTATION_UNLOCK (52),
|
||||
QUEST_COND_LUA_NOTIFY (53),
|
||||
QUEST_COND_CUR_CLIMATE (54),
|
||||
QUEST_COND_ACTIVITY_END (55),
|
||||
QUEST_COND_COOP_POINT_RUNNING (56),
|
||||
QUEST_COND_GADGET_TALK_STATE_EQUAL (57),
|
||||
QUEST_COND_AVATAR_FETTER_GT (58),
|
||||
QUEST_COND_AVATAR_FETTER_EQ (59),
|
||||
QUEST_COND_AVATAR_FETTER_LT (60),
|
||||
QUEST_COND_NEW_HOMEWORLD_MOUDLE_UNLOCK (61),
|
||||
QUEST_COND_NEW_HOMEWORLD_LEVEL_REWARD (62),
|
||||
QUEST_COND_NEW_HOMEWORLD_MAKE_FINISH (63),
|
||||
QUEST_COND_HOMEWORLD_NPC_EVENT (64),
|
||||
QUEST_COND_TIME_VAR_GT_EQ (65),
|
||||
QUEST_COND_TIME_VAR_PASS_DAY (66),
|
||||
QUEST_COND_HOMEWORLD_NPC_NEW_TALK (67),
|
||||
QUEST_COND_PLAYER_CHOOSE_MALE (68),
|
||||
QUEST_COND_HISTORY_GOT_ANY_ITEM (69),
|
||||
QUEST_COND_LEARNED_RECIPE (70),
|
||||
QUEST_COND_LUNARITE_REGION_UNLOCKED (71),
|
||||
QUEST_COND_LUNARITE_HAS_REGION_HINT_COUNT (72),
|
||||
QUEST_COND_LUNARITE_COLLECT_FINISH (73),
|
||||
QUEST_COND_LUNARITE_MARK_ALL_FINISH (74),
|
||||
QUEST_COND_NEW_HOMEWORLD_SHOP_ITEM (75),
|
||||
QUEST_COND_SCENE_POINT_UNLOCK (76),
|
||||
QUEST_COND_SCENE_LEVEL_TAG_EQ (77),
|
||||
|
||||
QUEST_CONTENT_NONE (0),
|
||||
QUEST_CONTENT_KILL_MONSTER (1),
|
||||
QUEST_CONTENT_COMPLETE_TALK (2),
|
||||
QUEST_CONTENT_MONSTER_DIE (3),
|
||||
QUEST_CONTENT_FINISH_PLOT (4),
|
||||
QUEST_CONTENT_OBTAIN_ITEM (5),
|
||||
QUEST_CONTENT_TRIGGER_FIRE (6),
|
||||
QUEST_CONTENT_CLEAR_GROUP_MONSTER (7),
|
||||
QUEST_CONTENT_NOT_FINISH_PLOT (8),
|
||||
QUEST_CONTENT_ENTER_DUNGEON (9),
|
||||
QUEST_CONTENT_ENTER_MY_WORLD (10),
|
||||
QUEST_CONTENT_FINISH_DUNGEON (11),
|
||||
QUEST_CONTENT_DESTROY_GADGET (12),
|
||||
QUEST_CONTENT_OBTAIN_MATERIAL_WITH_SUBTYPE (13),
|
||||
QUEST_CONTENT_NICK_NAME (14),
|
||||
QUEST_CONTENT_WORKTOP_SELECT (15),
|
||||
QUEST_CONTENT_SEAL_BATTLE_RESULT (16),
|
||||
QUEST_CONTENT_ENTER_ROOM (17),
|
||||
QUEST_CONTENT_GAME_TIME_TICK (18),
|
||||
QUEST_CONTENT_FAIL_DUNGEON (19),
|
||||
QUEST_CONTENT_LUA_NOTIFY (20),
|
||||
QUEST_CONTENT_TEAM_DEAD (21),
|
||||
QUEST_CONTENT_COMPLETE_ANY_TALK (22),
|
||||
QUEST_CONTENT_UNLOCK_TRANS_POINT (23),
|
||||
QUEST_CONTENT_ADD_QUEST_PROGRESS (24),
|
||||
QUEST_CONTENT_INTERACT_GADGET (25),
|
||||
QUEST_CONTENT_DAILY_TASK_COMP_FINISH (26),
|
||||
QUEST_CONTENT_FINISH_ITEM_GIVING (27),
|
||||
QUEST_CONTENT_SKILL (107),
|
||||
QUEST_CONTENT_CITY_LEVEL_UP (109),
|
||||
QUEST_CONTENT_PATTERN_GROUP_CLEAR_MONSTER (110),
|
||||
QUEST_CONTENT_ITEM_LESS_THAN (111),
|
||||
QUEST_CONTENT_PLAYER_LEVEL_UP (112),
|
||||
QUEST_CONTENT_DUNGEON_OPEN_STATUE (113),
|
||||
QUEST_CONTENT_UNLOCK_AREA (114),
|
||||
QUEST_CONTENT_OPEN_CHEST_WITH_GADGET_ID (115),
|
||||
QUEST_CONTENT_UNLOCK_TRANS_POINT_WITH_TYPE (116),
|
||||
QUEST_CONTENT_FINISH_DAILY_DUNGEON (117),
|
||||
QUEST_CONTENT_FINISH_WEEKLY_DUNGEON (118),
|
||||
QUEST_CONTENT_QUEST_VAR_EQUAL (119),
|
||||
QUEST_CONTENT_QUEST_VAR_GREATER (120),
|
||||
QUEST_CONTENT_QUEST_VAR_LESS (121),
|
||||
QUEST_CONTENT_OBTAIN_VARIOUS_ITEM (122),
|
||||
QUEST_CONTENT_FINISH_TOWER_LEVEL (123),
|
||||
QUEST_CONTENT_BARGAIN_SUCC (124),
|
||||
QUEST_CONTENT_BARGAIN_FAIL (125),
|
||||
QUEST_CONTENT_ITEM_LESS_THAN_BARGAIN (126),
|
||||
QUEST_CONTENT_ACTIVITY_TRIGGER_FAILED (127),
|
||||
QUEST_CONTENT_MAIN_COOP_ENTER_SAVE_POINT (128),
|
||||
QUEST_CONTENT_ANY_MANUAL_TRANSPORT (129),
|
||||
QUEST_CONTENT_USE_ITEM (130),
|
||||
QUEST_CONTENT_MAIN_COOP_ENTER_ANY_SAVE_POINT (131),
|
||||
QUEST_CONTENT_ENTER_MY_HOME_WORLD (132),
|
||||
QUEST_CONTENT_ENTER_MY_WORLD_SCENE (133),
|
||||
QUEST_CONTENT_TIME_VAR_GT_EQ (134),
|
||||
QUEST_CONTENT_TIME_VAR_PASS_DAY (135),
|
||||
QUEST_CONTENT_QUEST_STATE_EQUAL (136),
|
||||
QUEST_CONTENT_QUEST_STATE_NOT_EQUAL (137),
|
||||
QUEST_CONTENT_UNLOCKED_RECIPE (138),
|
||||
QUEST_CONTENT_NOT_UNLOCKED_RECIPE (139),
|
||||
QUEST_CONTENT_FISHING_SUCC (140),
|
||||
QUEST_CONTENT_ENTER_ROGUE_DUNGEON (141),
|
||||
QUEST_CONTENT_USE_WIDGET (142),
|
||||
QUEST_CONTENT_CAPTURE_SUCC (143),
|
||||
QUEST_CONTENT_CAPTURE_USE_CAPTURETAG_LIST (144),
|
||||
QUEST_CONTENT_CAPTURE_USE_MATERIAL_LIST (145),
|
||||
QUEST_CONTENT_ENTER_VEHICLE (147),
|
||||
QUEST_CONTENT_SCENE_LEVEL_TAG_EQ (148),
|
||||
QUEST_CONTENT_LEAVE_SCENE (149),
|
||||
QUEST_CONTENT_LEAVE_SCENE_RANGE (150),
|
||||
QUEST_CONTENT_IRODORI_FINISH_FLOWER_COMBINATION (151),
|
||||
QUEST_CONTENT_IRODORI_POETRY_REACH_MIN_PROGRESS (152),
|
||||
QUEST_CONTENT_IRODORI_POETRY_FINISH_FILL_POETRY (153),
|
||||
|
||||
QUEST_EXEC_NONE (0),
|
||||
QUEST_EXEC_DEL_PACK_ITEM (1),
|
||||
QUEST_EXEC_UNLOCK_POINT (2),
|
||||
QUEST_EXEC_UNLOCK_AREA (3),
|
||||
QUEST_EXEC_UNLOCK_FORCE (4),
|
||||
QUEST_EXEC_LOCK_FORCE (5),
|
||||
QUEST_EXEC_CHANGE_AVATAR_ELEMET (6),
|
||||
QUEST_EXEC_REFRESH_GROUP_MONSTER (7),
|
||||
QUEST_EXEC_SET_IS_FLYABLE (8),
|
||||
QUEST_EXEC_SET_IS_WEATHER_LOCKED (9),
|
||||
QUEST_EXEC_SET_IS_GAME_TIME_LOCKED (10),
|
||||
QUEST_EXEC_SET_IS_TRANSFERABLE (11),
|
||||
QUEST_EXEC_GRANT_TRIAL_AVATAR (12),
|
||||
QUEST_EXEC_OPEN_BORED (13),
|
||||
QUEST_EXEC_ROLLBACK_QUEST (14),
|
||||
QUEST_EXEC_NOTIFY_GROUP_LUA (15),
|
||||
QUEST_EXEC_SET_OPEN_STATE (16),
|
||||
QUEST_EXEC_LOCK_POINT (17),
|
||||
QUEST_EXEC_DEL_PACK_ITEM_BATCH (18),
|
||||
QUEST_EXEC_REFRESH_GROUP_SUITE (19),
|
||||
QUEST_EXEC_REMOVE_TRIAL_AVATAR (20),
|
||||
QUEST_EXEC_SET_GAME_TIME (21),
|
||||
QUEST_EXEC_SET_WEATHER_GADGET (22),
|
||||
QUEST_EXEC_ADD_QUEST_PROGRESS (23),
|
||||
QUEST_EXEC_NOTIFY_DAILY_TASK (24),
|
||||
QUEST_EXEC_CREATE_PATTERN_GROUP (25),
|
||||
QUEST_EXEC_REMOVE_PATTERN_GROUP (26),
|
||||
QUEST_EXEC_REFRESH_GROUP_SUITE_RANDOM (27),
|
||||
QUEST_EXEC_ACTIVE_ITEM_GIVING (28),
|
||||
QUEST_EXEC_DEL_ALL_SPECIFIC_PACK_ITEM (29),
|
||||
QUEST_EXEC_ROLLBACK_PARENT_QUEST (30),
|
||||
QUEST_EXEC_LOCK_AVATAR_TEAM (31),
|
||||
QUEST_EXEC_UNLOCK_AVATAR_TEAM (32),
|
||||
QUEST_EXEC_UPDATE_PARENT_QUEST_REWARD_INDEX (33),
|
||||
QUEST_EXEC_SET_DAILY_TASK_VAR (34),
|
||||
QUEST_EXEC_INC_DAILY_TASK_VAR (35),
|
||||
QUEST_EXEC_DEC_DAILY_TASK_VAR (36),
|
||||
QUEST_EXEC_ACTIVE_ACTIVITY_COND_STATE (37),
|
||||
QUEST_EXEC_INACTIVE_ACTIVITY_COND_STATE (38),
|
||||
QUEST_EXEC_ADD_CUR_AVATAR_ENERGY (39),
|
||||
QUEST_EXEC_START_BARGAIN (41),
|
||||
QUEST_EXEC_STOP_BARGAIN (42),
|
||||
QUEST_EXEC_SET_QUEST_GLOBAL_VAR (43),
|
||||
QUEST_EXEC_INC_QUEST_GLOBAL_VAR (44),
|
||||
QUEST_EXEC_DEC_QUEST_GLOBAL_VAR (45),
|
||||
QUEST_EXEC_REGISTER_DYNAMIC_GROUP (46),
|
||||
QUEST_EXEC_UNREGISTER_DYNAMIC_GROUP (47),
|
||||
QUEST_EXEC_SET_QUEST_VAR (48),
|
||||
QUEST_EXEC_INC_QUEST_VAR (49),
|
||||
QUEST_EXEC_DEC_QUEST_VAR (50),
|
||||
QUEST_EXEC_RANDOM_QUEST_VAR (51),
|
||||
QUEST_EXEC_ACTIVATE_SCANNING_PIC (52),
|
||||
QUEST_EXEC_RELOAD_SCENE_TAG (53),
|
||||
QUEST_EXEC_REGISTER_DYNAMIC_GROUP_ONLY (54),
|
||||
QUEST_EXEC_CHANGE_SKILL_DEPOT (55),
|
||||
QUEST_EXEC_ADD_SCENE_TAG (56),
|
||||
QUEST_EXEC_DEL_SCENE_TAG (57),
|
||||
QUEST_EXEC_INIT_TIME_VAR (58),
|
||||
QUEST_EXEC_CLEAR_TIME_VAR (59),
|
||||
QUEST_EXEC_MODIFY_CLIMATE_AREA (60),
|
||||
QUEST_EXEC_GRANT_TRIAL_AVATAR_AND_LOCK_TEAM (61),
|
||||
QUEST_EXEC_CHANGE_MAP_AREA_STATE (62),
|
||||
QUEST_EXEC_DEACTIVE_ITEM_GIVING (63),
|
||||
QUEST_EXEC_CHANGE_SCENE_LEVEL_TAG (64),
|
||||
QUEST_EXEC_UNLOCK_PLAYER_WORLD_SCENE (65),
|
||||
QUEST_EXEC_LOCK_PLAYER_WORLD_SCENE (66),
|
||||
QUEST_EXEC_FAIL_MAINCOOP (67),
|
||||
QUEST_EXEC_MODIFY_WEATHER_AREA (68);
|
||||
|
||||
private final int value;
|
||||
|
||||
QuestTrigger(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
public enum QuestType {
|
||||
AQ (0),
|
||||
FQ (1),
|
||||
LQ (2),
|
||||
EQ (3),
|
||||
DQ (4),
|
||||
IQ (5),
|
||||
VQ (6),
|
||||
WQ (7);
|
||||
|
||||
private final int value;
|
||||
|
||||
QuestType(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package emu.grasscutter.game.quest.enums;
|
||||
|
||||
public enum ShowQuestGuideType {
|
||||
QUEST_GUIDE_ITEM_ENABLE (0),
|
||||
QUEST_GUIDE_ITEM_DISABLE (1),
|
||||
QUEST_GUIDE_ITEM_MOVE_HIDE (2);
|
||||
|
||||
private final int value;
|
||||
|
||||
ShowQuestGuideType(int id) {
|
||||
this.value = id;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package emu.grasscutter.game.quest.handlers;
|
||||
|
||||
import emu.grasscutter.data.def.QuestData.QuestCondition;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
|
||||
public abstract class QuestBaseHandler {
|
||||
|
||||
public abstract boolean execute(GameQuest quest, QuestCondition condition, int... params);
|
||||
|
||||
}
|
@ -39,7 +39,8 @@ public class TowerScheduleManager {
|
||||
public TowerScheduleData getCurrentTowerScheduleData(){
|
||||
var data = GameData.getTowerScheduleDataMap().get(towerScheduleConfig.getScheduleId());
|
||||
if(data == null){
|
||||
Grasscutter.getLogger().error("Could not get current tower schedule data by config:{}", towerScheduleConfig);
|
||||
Grasscutter.getLogger().error("Could not get current tower schedule data by schedule id {}, please check your resource files",
|
||||
towerScheduleConfig.getScheduleId());
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -10,6 +10,7 @@ import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.player.Player.SceneLoadState;
|
||||
import emu.grasscutter.game.props.EnterReason;
|
||||
import emu.grasscutter.game.props.EntityIdType;
|
||||
import emu.grasscutter.game.props.SceneType;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.def.DungeonData;
|
||||
import emu.grasscutter.data.def.SceneData;
|
||||
@ -267,6 +268,9 @@ public class World implements Iterable<Player> {
|
||||
enterReason = EnterReason.DungeonEnter;
|
||||
} else if (oldScene == newScene) {
|
||||
enterType = EnterType.ENTER_GOTO;
|
||||
} else if (newScene.getSceneType() == SceneType.SCENE_HOME_WORLD) {
|
||||
// Home
|
||||
enterType = EnterType.ENTER_SELF_HOME;
|
||||
}
|
||||
|
||||
// Teleport packet
|
||||
|
@ -14,6 +14,8 @@ import emu.grasscutter.game.managers.ChatManager;
|
||||
import emu.grasscutter.game.managers.InventoryManager;
|
||||
import emu.grasscutter.game.managers.MultiplayerManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.ServerQuestHandler;
|
||||
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
|
||||
import emu.grasscutter.game.shop.ShopManager;
|
||||
import emu.grasscutter.game.tower.TowerScheduleManager;
|
||||
import emu.grasscutter.game.world.World;
|
||||
@ -37,7 +39,8 @@ import static emu.grasscutter.Configuration.*;
|
||||
public final class GameServer extends KcpServer {
|
||||
private final InetSocketAddress address;
|
||||
private final GameServerPacketHandler packetHandler;
|
||||
|
||||
private final ServerQuestHandler questHandler;
|
||||
|
||||
private final Map<Integer, Player> players;
|
||||
private final Set<World> worlds;
|
||||
|
||||
@ -68,6 +71,7 @@ public final class GameServer extends KcpServer {
|
||||
this.setServerInitializer(new GameServerInitializer(this));
|
||||
this.address = address;
|
||||
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
|
||||
this.questHandler = new ServerQuestHandler();
|
||||
this.players = new ConcurrentHashMap<>();
|
||||
this.worlds = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
@ -91,6 +95,10 @@ public final class GameServer extends KcpServer {
|
||||
return packetHandler;
|
||||
}
|
||||
|
||||
public ServerQuestHandler getQuestHandler() {
|
||||
return questHandler;
|
||||
}
|
||||
|
||||
public Map<Integer, Player> getPlayers() {
|
||||
return players;
|
||||
}
|
||||
|
@ -252,6 +252,7 @@ public class GameSession extends KcpChannel {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
data.release();
|
||||
packet.release();
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,13 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerRunMode;
|
||||
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*;
|
||||
import emu.grasscutter.net.proto.RegionInfoOuterClass;
|
||||
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
|
||||
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
|
||||
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
|
||||
import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
|
||||
import emu.grasscutter.server.http.Router;
|
||||
import emu.grasscutter.utils.Crypto;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import express.Express;
|
||||
@ -30,45 +33,24 @@ import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*;
|
||||
* Handles requests related to region queries.
|
||||
*/
|
||||
public final class RegionHandler implements Router {
|
||||
private String regionQuery = "";
|
||||
private String regionList = "";
|
||||
|
||||
private static final Map<String, RegionData> regions = new ConcurrentHashMap<>();
|
||||
private static String regionListResponse;
|
||||
|
||||
public RegionHandler() {
|
||||
try { // Read & initialize region data.
|
||||
this.readRegionData();
|
||||
this.initialize();
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.getLogger().error("Failed to initialize region data.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads initial region data.
|
||||
*/
|
||||
private void readRegionData() {
|
||||
File file;
|
||||
|
||||
file = new File(DATA("query_region_list.txt"));
|
||||
if (file.exists())
|
||||
this.regionList = new String(FileUtils.read(file));
|
||||
else Grasscutter.getLogger().error("[Dispatch] 'query_region_list' not found!");
|
||||
|
||||
file = new File(DATA("query_cur_region.txt"));
|
||||
if (file.exists())
|
||||
regionQuery = new String(FileUtils.read(file));
|
||||
else Grasscutter.getLogger().warn("[Dispatch] 'query_cur_region' not found!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures region data according to configuration.
|
||||
*/
|
||||
private void initialize() throws InvalidProtocolBufferException {
|
||||
// Decode the initial region query.
|
||||
byte[] queryBase64 = Base64.getDecoder().decode(this.regionQuery);
|
||||
QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(queryBase64);
|
||||
private void initialize() {
|
||||
String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
|
||||
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
|
||||
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
|
||||
|
||||
// Create regions.
|
||||
List<RegionSimpleInfo> servers = new ArrayList<>();
|
||||
@ -87,37 +69,33 @@ public final class RegionHandler implements Router {
|
||||
Grasscutter.getLogger().error("Region name already in use.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create a region identifier.
|
||||
var identifier = RegionSimpleInfo.newBuilder()
|
||||
.setName(region.Name).setTitle(region.Title)
|
||||
.setType("DEV_PUBLIC").setDispatchUrl(
|
||||
"http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
|
||||
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
|
||||
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort)
|
||||
+ "/query_cur_region/" + region.Name)
|
||||
.setName(region.Name).setTitle(region.Title).setType("DEV_PUBLIC")
|
||||
.setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name)
|
||||
.build();
|
||||
usedNames.add(region.Name); servers.add(identifier);
|
||||
|
||||
// Create a region info object.
|
||||
var regionInfo = regionQuery.getRegionInfo().toBuilder()
|
||||
var regionInfo = RegionInfo.newBuilder()
|
||||
.setGateserverIp(region.Ip).setGateserverPort(region.Port)
|
||||
.setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin")))
|
||||
.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
|
||||
.build();
|
||||
// Create an updated region query.
|
||||
var updatedQuery = regionQuery.toBuilder().setRegionInfo(regionInfo).build();
|
||||
var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build();
|
||||
regions.put(region.Name, new RegionData(updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray())));
|
||||
});
|
||||
|
||||
// Decode the initial region list.
|
||||
byte[] listBase64 = Base64.getDecoder().decode(this.regionList);
|
||||
QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.parseFrom(listBase64);
|
||||
// Create a config object.
|
||||
byte[] customConfig = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes();
|
||||
Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key.
|
||||
|
||||
// Create an updated region list.
|
||||
QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder()
|
||||
.addAllRegionList(servers)
|
||||
.setClientSecretKey(regionList.getClientSecretKey())
|
||||
.setClientCustomConfigEncrypted(regionList.getClientCustomConfigEncrypted())
|
||||
.setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
|
||||
.setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig))
|
||||
.setEnableLoginPc(true).build();
|
||||
|
||||
// Set the region list response.
|
||||
|
@ -3,6 +3,8 @@ package emu.grasscutter.server.http.handlers;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.server.http.objects.HttpJsonResponse;
|
||||
import emu.grasscutter.server.http.Router;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import express.Express;
|
||||
import express.http.Request;
|
||||
import express.http.Response;
|
||||
@ -11,6 +13,7 @@ import io.javalin.Javalin;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
|
||||
import static emu.grasscutter.Configuration.DATA;
|
||||
@ -19,6 +22,18 @@ import static emu.grasscutter.Configuration.DATA;
|
||||
* Handles requests related to the announcements page.
|
||||
*/
|
||||
public final class AnnouncementsHandler implements Router {
|
||||
private static String template, swjs, vue;
|
||||
|
||||
public AnnouncementsHandler() {
|
||||
var templateFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/index.html")));
|
||||
var swjsFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/sw.js")));
|
||||
var vueFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/vue.min.js")));
|
||||
|
||||
template = templateFile.exists() ? new String(FileUtils.read(template)) : null;
|
||||
swjs = swjsFile.exists() ? new String(FileUtils.read(swjs)) : null;
|
||||
vue = vueFile.exists() ? new String(FileUtils.read(vueFile)) : null;
|
||||
}
|
||||
|
||||
@Override public void applyRoutes(Express express, Javalin handle) {
|
||||
// hk4e-api-os.hoyoverse.com
|
||||
express.all("/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}"));
|
||||
@ -30,14 +45,45 @@ public final class AnnouncementsHandler implements Router {
|
||||
express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement);
|
||||
// hk4e-sdk-os.hoyoverse.com
|
||||
express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}"));
|
||||
|
||||
express.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources);
|
||||
express.get("/sw.js", AnnouncementsHandler::getPageResources);
|
||||
express.get("/dora/lib/vue/2.6.11/vue.min.js", AnnouncementsHandler::getPageResources);
|
||||
}
|
||||
|
||||
private static void getAnnouncement(Request request, Response response) {
|
||||
if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) {
|
||||
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(DATA("GameAnnouncement.json"))) +"}");
|
||||
String data = readToString(Paths.get(DATA("GameAnnouncement.json")).toFile());
|
||||
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + data + "}");
|
||||
} else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) {
|
||||
String data = readToString(new File(DATA("GameAnnouncementList.json"))).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis()));
|
||||
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}");
|
||||
String data = readToString(Paths.get(DATA("GameAnnouncementList.json")).toFile())
|
||||
.replace("System.currentTimeMillis()", String.valueOf(System.currentTimeMillis()));
|
||||
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void getPageResources(Request request, Response response) {
|
||||
var path = request.path();
|
||||
switch(path) {
|
||||
case "/sw.js" -> response.send(swjs);
|
||||
case "/hk4e/announcement/index.html" -> response.send(template);
|
||||
case "/dora/lib/vue/2.6.11/vue.min.js" -> response.send(vue);
|
||||
|
||||
default -> {
|
||||
File renderFile = new File(Utils.toFilePath(DATA(path)));
|
||||
if(!renderFile.exists()) {
|
||||
Grasscutter.getLogger().info("File not exist: " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
String ext = path.substring(path.lastIndexOf(".") + 1);
|
||||
if ("css".equals(ext)) {
|
||||
response.type("text/css");
|
||||
response.send(FileUtils.read(renderFile));
|
||||
} else {
|
||||
response.send(FileUtils.read(renderFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,10 @@ package emu.grasscutter.server.http.handlers;
|
||||
|
||||
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.player.Player;
|
||||
import emu.grasscutter.server.http.Router;
|
||||
import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
@ -13,8 +17,12 @@ import io.javalin.Javalin;
|
||||
import io.javalin.http.staticfiles.Location;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static emu.grasscutter.Configuration.DATA;
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
/**
|
||||
* Handles all gacha-related HTTP requests.
|
||||
@ -22,7 +30,8 @@ import static emu.grasscutter.Configuration.DATA;
|
||||
public final class GachaHandler implements Router {
|
||||
private final String gachaMappings;
|
||||
|
||||
private static String frontendTemplate = "{{REPLACE_RECORD}}";
|
||||
private static String recordsTemplate = "";
|
||||
private static String detailsTemplate = "";
|
||||
|
||||
public GachaHandler() {
|
||||
this.gachaMappings = Utils.toFilePath(DATA("/gacha_mappings.js"));
|
||||
@ -35,12 +44,15 @@ public final class GachaHandler implements Router {
|
||||
}
|
||||
|
||||
var templateFile = new File(DATA("/gacha_records.html"));
|
||||
if(templateFile.exists())
|
||||
frontendTemplate = new String(FileUtils.read(templateFile));
|
||||
recordsTemplate = templateFile.exists() ? new String(FileUtils.read(templateFile)) : "{{REPLACE_RECORD}}";
|
||||
|
||||
templateFile = new File(Utils.toFilePath(DATA("/gacha_details.html")));
|
||||
detailsTemplate = templateFile.exists() ? new String(FileUtils.read(templateFile)) : null;
|
||||
}
|
||||
|
||||
@Override public void applyRoutes(Express express, Javalin handle) {
|
||||
express.get("/gacha", GachaHandler::gachaRecords);
|
||||
express.get("/gacha/details", GachaHandler::gachaDetails);
|
||||
|
||||
express.useStaticFallback("/gacha/mappings", this.gachaMappings, Location.EXTERNAL);
|
||||
}
|
||||
@ -63,9 +75,62 @@ public final class GachaHandler implements Router {
|
||||
String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), gachaType, page).toString();
|
||||
long maxPage = DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType);
|
||||
|
||||
response.send(frontendTemplate
|
||||
response.send(recordsTemplate
|
||||
.replace("{{REPLACE_RECORD}}", records)
|
||||
.replace("{{REPLACE_MAXPAGE}}", String.valueOf(maxPage)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void gachaDetails(Request request, Response response) {
|
||||
String template = detailsTemplate;
|
||||
|
||||
// Get player info (for langauge).
|
||||
String sessionKey = request.query("s");
|
||||
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
|
||||
Player player = Grasscutter.getGameServer().getPlayerByUid(account.getPlayerUid());
|
||||
|
||||
// If the template was not loaded, return an error.
|
||||
if (detailsTemplate == null) {
|
||||
response.send(translate(player, "gacha.details.template_missing"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add translated title etc. to the page.
|
||||
template = template.replace("{{TITLE}}", translate(player, "gacha.details.title"))
|
||||
.replace("{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars"))
|
||||
.replace("{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars"))
|
||||
.replace("{{AVAILABLE_THREE_STARS}}", translate(player, "gacha.details.available_three_stars"))
|
||||
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
|
||||
|
||||
// Get the banner info for the banner we want.
|
||||
int gachaType = Integer.parseInt(request.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.getRateUpItems5()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems5Pool1()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems5Pool2()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
|
||||
|
||||
template = template.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]");
|
||||
|
||||
// Add 4-star items.
|
||||
Set<String> fourStarItems = new LinkedHashSet<>();
|
||||
|
||||
Arrays.stream(banner.getRateUpItems4()).forEach(i -> fourStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems4Pool1()).forEach(i -> fourStarItems.add(Integer.toString(i)));
|
||||
Arrays.stream(banner.getFallbackItems4Pool2()).forEach(i -> fourStarItems.add(Integer.toString(i)));
|
||||
|
||||
template = template.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]");
|
||||
|
||||
// Add 3-star items.
|
||||
Set<String> threeStarItems = new LinkedHashSet<>();
|
||||
Arrays.stream(banner.getFallbackItems3()).forEach(i -> threeStarItems.add(Integer.toString(i)));
|
||||
template = template.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
|
||||
|
||||
// Done.
|
||||
response.send(template);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import emu.grasscutter.server.packet.send.PacketBuyGoodsRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketStoreItemChangeNotify;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -56,36 +56,13 @@ public class HandlerBuyGoodsReq extends PacketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sg.getScoin() > 0 && session.getPlayer().getMora() < buyGoodsReq.getBoughtNum() * sg.getScoin()) {
|
||||
List<ItemParamData> costs = new ArrayList<ItemParamData>(sg.getCostItemList()); // Can this even be null?
|
||||
costs.add(new ItemParamData(202, sg.getScoin()));
|
||||
costs.add(new ItemParamData(201, sg.getHcoin()));
|
||||
costs.add(new ItemParamData(203, sg.getMcoin()));
|
||||
if (!session.getPlayer().getInventory().payItems(costs.toArray(new ItemParamData[0]), buyGoodsReq.getBoughtNum())) {
|
||||
return;
|
||||
}
|
||||
if (sg.getHcoin() > 0 && session.getPlayer().getPrimogems() < buyGoodsReq.getBoughtNum() * sg.getHcoin()) {
|
||||
return;
|
||||
}
|
||||
if (sg.getMcoin() > 0 && session.getPlayer().getCrystals() < buyGoodsReq.getBoughtNum() * sg.getMcoin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
HashMap<GameItem, Integer> itemsCache = new HashMap<>();
|
||||
if (sg.getCostItemList() != null) {
|
||||
for (ItemParamData p : sg.getCostItemList()) {
|
||||
Optional<GameItem> invItem = session.getPlayer().getInventory().getItems().values().stream().filter(x -> x.getItemId() == p.getId()).findFirst();
|
||||
if (invItem.isEmpty() || invItem.get().getCount() < p.getCount())
|
||||
return;
|
||||
itemsCache.put(invItem.get(), p.getCount() * buyGoodsReq.getBoughtNum());
|
||||
}
|
||||
}
|
||||
|
||||
session.getPlayer().setMora(session.getPlayer().getMora() - buyGoodsReq.getBoughtNum() * sg.getScoin());
|
||||
session.getPlayer().setPrimogems(session.getPlayer().getPrimogems() - buyGoodsReq.getBoughtNum() * sg.getHcoin());
|
||||
session.getPlayer().setCrystals(session.getPlayer().getCrystals() - buyGoodsReq.getBoughtNum() * sg.getMcoin());
|
||||
|
||||
if (!itemsCache.isEmpty()) {
|
||||
for (GameItem gi : itemsCache.keySet()) {
|
||||
session.getPlayer().getInventory().removeItem(gi, itemsCache.get(gi));
|
||||
}
|
||||
itemsCache.clear();
|
||||
}
|
||||
|
||||
session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum(), ShopManager.getShopNextRefreshTime(sg));
|
||||
GameItem item = new GameItem(GameData.getItemDataMap().get(sg.getGoodsItem().getId()));
|
||||
|
@ -11,12 +11,6 @@ import emu.grasscutter.server.game.GameSession;
|
||||
public class HandlerEnterTransPointRegionNotify extends PacketHandler {
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception{
|
||||
Player player = session.getPlayer();
|
||||
SotSManager sotsManager = player.getSotSManager();
|
||||
|
||||
sotsManager.refillSpringVolume();
|
||||
sotsManager.autoRevive(session);
|
||||
sotsManager.scheduleAutoRecover(session);
|
||||
// TODO: allow interaction with the SotS?
|
||||
session.getPlayer().getSotSManager().handleEnterTransPointRegionNotify();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.managers.SotSManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
@ -11,8 +12,6 @@ import emu.grasscutter.server.game.GameSession;
|
||||
public class HandlerExitTransPointRegionNotify extends PacketHandler {
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception{
|
||||
Player player = session.getPlayer();
|
||||
SotSManager sotsManager = player.getSotSManager();
|
||||
sotsManager.cancelAutoRecover();
|
||||
session.getPlayer().getSotSManager().handleExitTransPointRegionNotify();
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,20 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketGetShopRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketGetWidgetSlotRsp;
|
||||
|
||||
@Opcodes(PacketOpcodes.GetWidgetSlotReq)
|
||||
public class HandlerGetWidgetSlotReq extends PacketHandler {
|
||||
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
// Unhandled
|
||||
Player player = session.getPlayer();
|
||||
session.send(new PacketGetWidgetSlotRsp(player));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.HomeChooseModuleReqOuterClass;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketHomeChooseModuleRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketHomeComfortInfoNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketPlayerHomeCompInfoNotify;
|
||||
|
||||
|
||||
@Opcodes(PacketOpcodes.HomeChooseModuleReq)
|
||||
public class HandlerHomeChooseModuleReq extends PacketHandler {
|
||||
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
HomeChooseModuleReqOuterClass.HomeChooseModuleReq req =
|
||||
HomeChooseModuleReqOuterClass.HomeChooseModuleReq.parseFrom(payload);
|
||||
session.getPlayer().addRealmList(req.getModuleId());
|
||||
session.getPlayer().setCurrentRealmId(req.getModuleId());
|
||||
session.send(new PacketHomeChooseModuleRsp(req.getModuleId()));
|
||||
session.send(new PacketPlayerHomeCompInfoNotify(session.getPlayer()));
|
||||
session.send(new PacketHomeComfortInfoNotify(session.getPlayer()));
|
||||
}
|
||||
}
|
@ -1,84 +1,17 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.game.managers.MapMarkManager.MapMark;
|
||||
import emu.grasscutter.game.managers.MapMarkManager.MapMarksManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.*;
|
||||
import emu.grasscutter.net.proto.MarkMapReqOuterClass.MarkMapReq;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketMarkMapRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketMarkNewNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify;
|
||||
import emu.grasscutter.utils.Position;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Opcodes(PacketOpcodes.MarkMapReq)
|
||||
public class HandlerMarkMapReq extends PacketHandler {
|
||||
|
||||
private static boolean isInt(String str) {
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unused")
|
||||
int x = Integer.parseInt(str);
|
||||
return true; // String is an Integer
|
||||
} catch (NumberFormatException e) {
|
||||
return false; // String is not an Integer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
MarkMapReq req = MarkMapReq.parseFrom(payload);
|
||||
MarkMapReq.Operation op = req.getOp();
|
||||
Player player = session.getPlayer();
|
||||
MapMarksManager mapMarksManager = player.getMapMarksManager();
|
||||
if (op == MarkMapReq.Operation.ADD) {
|
||||
MapMark newMapMark = new MapMark(req.getMark());
|
||||
// keep teleporting functionality on fishhook mark.
|
||||
if (newMapMark.getMapMarkPointType() == MapMarkPointTypeOuterClass.MapMarkPointType.MAP_MARK_POINT_TYPE_FISH_POOL) {
|
||||
teleport(player, newMapMark);
|
||||
return;
|
||||
}
|
||||
if (mapMarksManager.addMapMark(newMapMark)) {
|
||||
player.save();
|
||||
}
|
||||
} else if (op == MarkMapReq.Operation.MOD) {
|
||||
MapMark newMapMark = new MapMark(req.getMark());
|
||||
if (mapMarksManager.removeMapMark(newMapMark.getPosition())) {
|
||||
if (mapMarksManager.addMapMark(newMapMark)) {
|
||||
player.save();
|
||||
}
|
||||
}
|
||||
} else if (op == MarkMapReq.Operation.DEL) {
|
||||
MapMark newMapMark = new MapMark(req.getMark());
|
||||
if (mapMarksManager.removeMapMark(newMapMark.getPosition())) {
|
||||
player.save();
|
||||
}
|
||||
} else if (op == MarkMapReq.Operation.GET) {
|
||||
// no-op
|
||||
}
|
||||
// send all marks to refresh client map view.
|
||||
HashMap<String, MapMark> mapMarks = mapMarksManager.getAllMapMarks();
|
||||
session.send(new PacketMarkMapRsp(player, mapMarks));
|
||||
}
|
||||
|
||||
private void teleport(Player player, MapMark mapMark) {
|
||||
float y = isInt(mapMark.getName()) ? Integer.parseInt(mapMark.getName()) : 300;
|
||||
float x = mapMark.getPosition().getX();
|
||||
float z = mapMark.getPosition().getZ();
|
||||
player.getPos().set(x, y, z);
|
||||
if (mapMark.getSceneId() != player.getSceneId()) {
|
||||
player.getWorld().transferPlayerToScene(player, mapMark.getSceneId(),
|
||||
player.getPos());
|
||||
} else {
|
||||
player.getScene().broadcastPacket(new PacketSceneEntityAppearNotify(player));
|
||||
}
|
||||
session.getPlayer().getMapMarksManager().handleMapMarkReq(req);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.quest.enums.QuestTrigger;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.NpcTalkReqOuterClass.NpcTalkReq;
|
||||
@ -14,6 +15,10 @@ public class HandlerNpcTalkReq extends PacketHandler {
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
NpcTalkReq req = NpcTalkReq.parseFrom(payload);
|
||||
|
||||
// Why are there 2 quest triggers that do the same thing...
|
||||
session.getPlayer().getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_COMPLETE_TALK, req.getTalkId());
|
||||
session.getPlayer().getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_FINISH_PLOT, req.getTalkId());
|
||||
|
||||
session.send(new PacketNpcTalkRsp(req.getNpcEntityId(), req.getTalkId(), req.getEntityId()));
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.SetWidgetSlotReqOuterClass;
|
||||
import emu.grasscutter.net.proto.WidgetSlotOpOuterClass;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketSetWidgetSlotRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketWidgetSlotChangeNotify;
|
||||
|
||||
@Opcodes(PacketOpcodes.SetWidgetSlotReq)
|
||||
public class HandlerSetWidgetSlotReq extends PacketHandler {
|
||||
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
SetWidgetSlotReqOuterClass.SetWidgetSlotReq req = SetWidgetSlotReqOuterClass.SetWidgetSlotReq.parseFrom(payload);
|
||||
|
||||
Player player = session.getPlayer();
|
||||
player.setWidgetId(req.getMaterialId());
|
||||
|
||||
// WidgetSlotChangeNotify op & slot key
|
||||
session.send(new PacketWidgetSlotChangeNotify(WidgetSlotOpOuterClass.WidgetSlotOp.DETACH));
|
||||
// WidgetSlotChangeNotify slot
|
||||
session.send(new PacketWidgetSlotChangeNotify(req.getMaterialId()));
|
||||
|
||||
// SetWidgetSlotRsp
|
||||
session.send(new PacketSetWidgetSlotRsp(req.getMaterialId()));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.TryEnterHomeReqOuterClass;
|
||||
import emu.grasscutter.scripts.data.SceneConfig;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketTryEnterHomeRsp;
|
||||
import emu.grasscutter.utils.Position;
|
||||
|
||||
@Opcodes(PacketOpcodes.TryEnterHomeReq)
|
||||
public class HandlerTryEnterHomeReq extends PacketHandler {
|
||||
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
TryEnterHomeReqOuterClass.TryEnterHomeReq req =
|
||||
TryEnterHomeReqOuterClass.TryEnterHomeReq.parseFrom(payload);
|
||||
|
||||
if (req.getTargetUid() != session.getPlayer().getUid()) {
|
||||
// I hope that tomorrow there will be a hero who can support multiplayer mode and write code like a poem
|
||||
session.send(new PacketTryEnterHomeRsp());
|
||||
return;
|
||||
}
|
||||
|
||||
int realmId = 2000 + session.getPlayer().getCurrentRealmId();
|
||||
|
||||
Scene scene = session.getPlayer().getWorld().getSceneById(realmId);
|
||||
Position pos = scene.getScriptManager().getConfig().born_pos;
|
||||
|
||||
session.getPlayer().getWorld().transferPlayerToScene(
|
||||
session.getPlayer(),
|
||||
realmId,
|
||||
pos
|
||||
);
|
||||
|
||||
|
||||
session.send(new PacketTryEnterHomeRsp(req.getTargetUid()));
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ public class HandlerVehicleInteractReq extends PacketHandler {
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
VehicleInteractReqOuterClass.VehicleInteractReq req = VehicleInteractReqOuterClass.VehicleInteractReq.parseFrom(payload);
|
||||
session.getPlayer().getStaminaManager().handleVehicleInteractReq(session, req.getEntityId(), req.getInteractType());
|
||||
session.send(new PacketVehicleInteractRsp(session.getPlayer(), req.getEntityId(), req.getInteractType()));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.def.GadgetData;
|
||||
import emu.grasscutter.game.entity.EntityVehicle;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.props.LifeState;
|
||||
import emu.grasscutter.net.packet.Opcodes;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.*;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketWidgetCoolDownNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketWidgetDoBagRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketWidgetGadgetDataNotify;
|
||||
import emu.grasscutter.utils.Position;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Opcodes(PacketOpcodes.WidgetDoBagReq)
|
||||
public class HandlerWidgetDoBagReq extends PacketHandler {
|
||||
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
WidgetDoBagReqOuterClass.WidgetDoBagReq req = WidgetDoBagReqOuterClass.WidgetDoBagReq.parseFrom(payload);
|
||||
switch (req.getMaterialId()) {
|
||||
case 220026 -> {
|
||||
GadgetData gadgetData = GameData.getGadgetDataMap().get(70500025);
|
||||
Position pos = new Position(req.getWidgetCreatorInfo().getLocationInfo().getPos());
|
||||
Position rot = new Position(req.getWidgetCreatorInfo().getLocationInfo().getRot());
|
||||
GameEntity entity = new EntityVehicle(
|
||||
session.getPlayer().getScene(),
|
||||
session.getPlayer(),
|
||||
gadgetData.getId(),
|
||||
0,
|
||||
pos,
|
||||
rot
|
||||
);
|
||||
|
||||
session.getPlayer().getScene().addEntity(entity);
|
||||
|
||||
session.send(new PacketWidgetGadgetDataNotify(70500025, List.of(entity.getId()))); // ???
|
||||
session.send(new PacketWidgetCoolDownNotify(15, System.currentTimeMillis() + 5000L, true));
|
||||
session.send(new PacketWidgetCoolDownNotify(15, System.currentTimeMillis() + 5000L, true));
|
||||
// Send twice, and I don't know why, Ask mhy
|
||||
session.send(new PacketWidgetDoBagRsp());
|
||||
}
|
||||
default -> {
|
||||
session.send(new PacketWidgetDoBagRsp());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.AllWidgetDataNotifyOuterClass.AllWidgetDataNotify;
|
||||
import emu.grasscutter.net.proto.LunchBoxDataOuterClass;
|
||||
import emu.grasscutter.net.proto.WidgetSlotDataOuterClass;
|
||||
import emu.grasscutter.net.proto.WidgetSlotTagOuterClass;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PacketAllWidgetDataNotify extends BasePacket {
|
||||
|
||||
public PacketAllWidgetDataNotify(Player player) {
|
||||
super(PacketOpcodes.AllWidgetDataNotify);
|
||||
|
||||
// TODO: Implement this
|
||||
|
||||
AllWidgetDataNotify.Builder proto = AllWidgetDataNotify.newBuilder()
|
||||
// If you want to implement this, feel free to do so. :)
|
||||
.setLunchBoxData(
|
||||
LunchBoxDataOuterClass.LunchBoxData.newBuilder().build()
|
||||
)
|
||||
// Maybe it's a little difficult, or it makes you upset :(
|
||||
.addAllOneoffGatherPointDetectorDataList(List.of())
|
||||
// So, goodbye, and hopefully sometime in the future o(* ̄▽ ̄*)ブ
|
||||
.addAllCoolDownGroupDataList(List.of())
|
||||
// I'll see your PR with a title that says (・∀・(・∀・(・∀・*)
|
||||
.addAllAnchorPointList(List.of())
|
||||
// "Complete implementation of widget functionality" b( ̄▽ ̄)d
|
||||
.addAllClientCollectorDataList(List.of())
|
||||
// Good luck, my boy.
|
||||
.addAllNormalCoolDownDataList(List.of());
|
||||
|
||||
if (player.getWidgetId() == null) {
|
||||
proto.addAllSlotList(List.of());
|
||||
} else {
|
||||
proto.addSlotList(
|
||||
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
|
||||
.setIsActive(true)
|
||||
.setMaterialId(player.getWidgetId())
|
||||
.build()
|
||||
);
|
||||
|
||||
proto.addSlotList(
|
||||
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
|
||||
.setTag(WidgetSlotTagOuterClass.WidgetSlotTag.WIDGET_SLOT_ATTACH_AVATAR)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
AllWidgetDataNotify protoData = proto.build();
|
||||
|
||||
this.setData(protoData);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.CodexDataFullNotifyOuterClass.CodexDataFullNotify;
|
||||
import emu.grasscutter.net.proto.CodexTypeDataOuterClass.CodexTypeData;
|
||||
import emu.grasscutter.net.proto.CodexTypeOuterClass;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
|
||||
public class PacketCodexDataFullNotify extends BasePacket {
|
||||
public PacketCodexDataFullNotify(Player player) {
|
||||
super(PacketOpcodes.CodexDataFullNotify, true);
|
||||
|
||||
//Quests
|
||||
CodexTypeData.Builder questTypeData = CodexTypeData.newBuilder()
|
||||
.setTypeValue(1);
|
||||
|
||||
//Tips
|
||||
CodexTypeData.Builder pushTipsTypeData = CodexTypeData.newBuilder()
|
||||
.setTypeValue(6);
|
||||
|
||||
//Views
|
||||
CodexTypeData.Builder viewTypeData = CodexTypeData.newBuilder()
|
||||
.setTypeValue(7);
|
||||
|
||||
//Weapons
|
||||
CodexTypeData.Builder weaponTypeData = CodexTypeData.newBuilder()
|
||||
.setTypeValue(2);
|
||||
|
||||
|
||||
player.getQuestManager().forEachMainQuest(mainQuest -> {
|
||||
if(mainQuest.isFinished()){
|
||||
var codexQuest = GameData.getCodexQuestIdMap().get(mainQuest.getParentQuestId());
|
||||
if(codexQuest != null){
|
||||
questTypeData.addCodexIdList(codexQuest.getId()).addAllHaveViewedList(Collections.singleton(true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CodexDataFullNotify.Builder proto = CodexDataFullNotify.newBuilder()
|
||||
.addTypeDataList(questTypeData.build())
|
||||
.addTypeDataList(pushTipsTypeData.build())
|
||||
.addTypeDataList(viewTypeData.build())
|
||||
.addTypeDataList(weaponTypeData);
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.CodexDataUpdateNotifyOuterClass.CodexDataUpdateNotify;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
|
||||
public class PacketCodexDataUpdateNotify extends BasePacket {
|
||||
public PacketCodexDataUpdateNotify(GameMainQuest quest) {
|
||||
super(PacketOpcodes.CodexDataUpdateNotify, true);
|
||||
var codexQuest = GameData.getCodexQuestIdMap().get(quest.getParentQuestId());
|
||||
if(codexQuest != null){
|
||||
CodexDataUpdateNotify proto = CodexDataUpdateNotify.newBuilder()
|
||||
.setTypeValue(1)
|
||||
.setId(codexQuest.getId())
|
||||
.build();
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package emu.grasscutter.server.packet.send;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.game.gacha.GachaBanner;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
@ -14,16 +15,18 @@ public class PacketDoGachaRsp extends BasePacket {
|
||||
public PacketDoGachaRsp(GachaBanner banner, List<GachaItem> list) {
|
||||
super(PacketOpcodes.DoGachaRsp);
|
||||
|
||||
ItemParamData costItem = banner.getCost(1);
|
||||
ItemParamData costItem10 = banner.getCost(10);
|
||||
DoGachaRsp p = DoGachaRsp.newBuilder()
|
||||
.setGachaType(banner.getGachaType())
|
||||
.setGachaScheduleId(banner.getScheduleId())
|
||||
.setGachaTimes(list.size())
|
||||
.setNewGachaRandom(12345)
|
||||
.setLeftGachaTimes(Integer.MAX_VALUE)
|
||||
.setCostItemId(banner.getCostItem())
|
||||
.setCostItemNum(1)
|
||||
.setTenCostItemId(banner.getCostItem())
|
||||
.setTenCostItemNum(10)
|
||||
.setCostItemId(costItem.getId())
|
||||
.setCostItemNum(costItem.getCount())
|
||||
.setTenCostItemId(costItem10.getId())
|
||||
.setTenCostItemNum(costItem10.getCount())
|
||||
.addAllGachaItemList(list)
|
||||
.build();
|
||||
|
||||
|
@ -0,0 +1,22 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.FinishedParentQuestNotifyOuterClass.FinishedParentQuestNotify;
|
||||
|
||||
public class PacketFinishedParentQuestNotify extends BasePacket {
|
||||
|
||||
public PacketFinishedParentQuestNotify(Player player) {
|
||||
super(PacketOpcodes.FinishedParentQuestNotify, true);
|
||||
|
||||
FinishedParentQuestNotify.Builder proto = FinishedParentQuestNotify.newBuilder();
|
||||
|
||||
for (GameMainQuest mainQuest : player.getQuestManager().getQuests().values()) {
|
||||
proto.addParentQuestList(mainQuest.toProto());
|
||||
}
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.FinishedParentQuestUpdateNotifyOuterClass.FinishedParentQuestUpdateNotify;
|
||||
|
||||
public class PacketFinishedParentQuestUpdateNotify extends BasePacket {
|
||||
|
||||
public PacketFinishedParentQuestUpdateNotify(GameMainQuest quest) {
|
||||
super(PacketOpcodes.FinishedParentQuestUpdateNotify);
|
||||
|
||||
FinishedParentQuestUpdateNotify proto = FinishedParentQuestUpdateNotify.newBuilder()
|
||||
.addParentQuestList(quest.toProto())
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.GetWidgetSlotRspOuterClass;
|
||||
import emu.grasscutter.net.proto.WidgetSlotDataOuterClass;
|
||||
import emu.grasscutter.net.proto.WidgetSlotTagOuterClass;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PacketGetWidgetSlotRsp extends BasePacket {
|
||||
|
||||
public PacketGetWidgetSlotRsp(Player player) {
|
||||
super(PacketOpcodes.GetWidgetSlotRsp);
|
||||
|
||||
GetWidgetSlotRspOuterClass.GetWidgetSlotRsp.Builder proto =
|
||||
GetWidgetSlotRspOuterClass.GetWidgetSlotRsp.newBuilder();
|
||||
|
||||
if (player.getWidgetId() == null) {
|
||||
proto.addAllSlotList(List.of());
|
||||
} else {
|
||||
proto.addSlotList(
|
||||
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
|
||||
.setIsActive(true)
|
||||
.setMaterialId(player.getWidgetId())
|
||||
.build()
|
||||
);
|
||||
|
||||
proto.addSlotList(
|
||||
WidgetSlotDataOuterClass.WidgetSlotData.newBuilder()
|
||||
.setTag(WidgetSlotTagOuterClass.WidgetSlotTag.WIDGET_SLOT_ATTACH_AVATAR)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
GetWidgetSlotRspOuterClass.GetWidgetSlotRsp protoData = proto.build();
|
||||
|
||||
this.setData(protoData);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.HomeChooseModuleRspOuterClass;
|
||||
|
||||
public class PacketHomeChooseModuleRsp extends BasePacket {
|
||||
|
||||
public PacketHomeChooseModuleRsp(int moduleId) {
|
||||
super(PacketOpcodes.HomeChooseModuleRsp);
|
||||
|
||||
HomeChooseModuleRspOuterClass.HomeChooseModuleRsp proto = HomeChooseModuleRspOuterClass.HomeChooseModuleRsp.newBuilder()
|
||||
.setRetcode(0)
|
||||
.setModuleId(moduleId)
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.HomeComfortInfoNotifyOuterClass;
|
||||
import emu.grasscutter.net.proto.HomeModuleComfortInfoOuterClass;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PacketHomeComfortInfoNotify extends BasePacket {
|
||||
|
||||
public PacketHomeComfortInfoNotify(Player player) {
|
||||
super(PacketOpcodes.HomeComfortInfoNotify);
|
||||
|
||||
if (player.getRealmList() == null) {
|
||||
// Do not send
|
||||
return;
|
||||
}
|
||||
|
||||
List<HomeModuleComfortInfoOuterClass.HomeModuleComfortInfo> comfortInfoList = new ArrayList<>();
|
||||
|
||||
for (int moduleId : player.getRealmList()) {
|
||||
comfortInfoList.add(
|
||||
HomeModuleComfortInfoOuterClass.HomeModuleComfortInfo.newBuilder()
|
||||
.setModuleId(moduleId)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
HomeComfortInfoNotifyOuterClass.HomeComfortInfoNotify proto = HomeComfortInfoNotifyOuterClass.HomeComfortInfoNotify
|
||||
.newBuilder()
|
||||
.addAllModuleInfoList(comfortInfoList)
|
||||
.build();
|
||||
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import java.util.*;
|
||||
|
||||
public class PacketMarkMapRsp extends BasePacket {
|
||||
|
||||
public PacketMarkMapRsp(Player player, HashMap<String, MapMark> mapMarks) {
|
||||
public PacketMarkMapRsp(HashMap<String, MapMark> mapMarks) {
|
||||
super(PacketOpcodes.MarkMapRsp);
|
||||
|
||||
MarkMapRspOuterClass.MarkMapRsp.Builder proto = MarkMapRspOuterClass.MarkMapRsp.newBuilder();
|
||||
|
@ -0,0 +1,32 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.PlayerHomeCompInfoNotifyOuterClass;
|
||||
import emu.grasscutter.net.proto.PlayerHomeCompInfoOuterClass;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PacketPlayerHomeCompInfoNotify extends BasePacket {
|
||||
|
||||
public PacketPlayerHomeCompInfoNotify(Player player) {
|
||||
super(PacketOpcodes.PlayerHomeCompInfoNotify);
|
||||
|
||||
if (player.getRealmList() == null) {
|
||||
// Do not send
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify proto = PlayerHomeCompInfoNotifyOuterClass.PlayerHomeCompInfoNotify.newBuilder()
|
||||
.setCompInfo(
|
||||
PlayerHomeCompInfoOuterClass.PlayerHomeCompInfo.newBuilder()
|
||||
.addAllUnlockedModuleIdList(player.getRealmList())
|
||||
.addAllLevelupRewardGotLevelList(List.of(1)) // Hardcoded
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -9,12 +9,10 @@ import emu.grasscutter.net.proto.PlayerLoginRspOuterClass.PlayerLoginRsp;
|
||||
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass;
|
||||
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.http.dispatch.RegionHandler;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
import static emu.grasscutter.Configuration.*;
|
||||
|
||||
@ -32,24 +30,14 @@ public class PacketPlayerLoginRsp extends BasePacket {
|
||||
if (SERVER.runMode == ServerRunMode.GAME_ONLY) {
|
||||
if (regionCache == null) {
|
||||
try {
|
||||
File file = new File(DATA("query_cur_region.txt"));
|
||||
String query_cur_region = "";
|
||||
if (file.exists()) {
|
||||
query_cur_region = new String(FileUtils.read(file));
|
||||
} else {
|
||||
Grasscutter.getLogger().warn("query_cur_region not found! Using default current region.");
|
||||
}
|
||||
|
||||
byte[] decodedCurRegion = Base64.getDecoder().decode(query_cur_region);
|
||||
QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp.parseFrom(decodedCurRegion);
|
||||
|
||||
RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder()
|
||||
// todo: we might want to push custom config to client
|
||||
RegionInfo serverRegion = RegionInfo.newBuilder()
|
||||
.setGateserverIp(lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress))
|
||||
.setGateserverPort(lr(GAME_INFO.accessPort, GAME_INFO.bindPort))
|
||||
.setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin")))
|
||||
.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
|
||||
.build();
|
||||
|
||||
regionCache = regionQuery.toBuilder().setRegionInfo(serverRegion).build();
|
||||
regionCache = QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(serverRegion).build();
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("Error while initializing region cache!", e);
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.game.quest.QuestManager;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.QuestListNotifyOuterClass.QuestListNotify;
|
||||
|
||||
public class PacketQuestListNotify extends BasePacket {
|
||||
|
||||
public PacketQuestListNotify(Player player) {
|
||||
super(PacketOpcodes.QuestListNotify, true);
|
||||
|
||||
QuestListNotify.Builder proto = QuestListNotify.newBuilder();
|
||||
|
||||
player.getQuestManager().forEachQuest(quest -> {
|
||||
proto.addQuestList(quest.toProto());
|
||||
});
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.QuestListUpdateNotifyOuterClass.QuestListUpdateNotify;
|
||||
|
||||
public class PacketQuestListUpdateNotify extends BasePacket {
|
||||
|
||||
public PacketQuestListUpdateNotify(GameQuest quest) {
|
||||
super(PacketOpcodes.QuestListUpdateNotify);
|
||||
|
||||
QuestListUpdateNotify proto = QuestListUpdateNotify.newBuilder()
|
||||
.addQuestList(quest.toProto())
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.QuestProgressUpdateNotifyOuterClass.QuestProgressUpdateNotify;
|
||||
|
||||
public class PacketQuestProgressUpdateNotify extends BasePacket {
|
||||
|
||||
public PacketQuestProgressUpdateNotify(GameQuest quest) {
|
||||
super(PacketOpcodes.QuestProgressUpdateNotify);
|
||||
|
||||
QuestProgressUpdateNotify.Builder proto = QuestProgressUpdateNotify.newBuilder().setQuestId(quest.getQuestId());
|
||||
|
||||
if (quest.getFinishProgressList() != null) {
|
||||
for (int i : quest.getFinishProgressList()) {
|
||||
proto.addFinishProgressList(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (quest.getFailProgressList() != null) {
|
||||
for (int i : quest.getFailProgressList()) {
|
||||
proto.addFailProgressList(i);
|
||||
}
|
||||
}
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameQuest;
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.ServerCondMeetQuestListUpdateNotifyOuterClass.ServerCondMeetQuestListUpdateNotify;
|
||||
|
||||
public class PacketServerCondMeetQuestListUpdateNotify extends BasePacket {
|
||||
|
||||
public PacketServerCondMeetQuestListUpdateNotify(Player player) {
|
||||
super(PacketOpcodes.ServerCondMeetQuestListUpdateNotify);
|
||||
|
||||
ServerCondMeetQuestListUpdateNotify.Builder proto = ServerCondMeetQuestListUpdateNotify.newBuilder();
|
||||
|
||||
player.getQuestManager().forEachQuest(quest -> {
|
||||
if (quest.getState().getValue() <= 2) {
|
||||
proto.addAddQuestIdList(quest.getQuestId());
|
||||
}
|
||||
});
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
|
||||
public PacketServerCondMeetQuestListUpdateNotify(GameQuest quest) {
|
||||
super(PacketOpcodes.ServerCondMeetQuestListUpdateNotify);
|
||||
|
||||
ServerCondMeetQuestListUpdateNotify proto = ServerCondMeetQuestListUpdateNotify.newBuilder()
|
||||
.addAddQuestIdList(quest.getQuestId())
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.SetWidgetSlotRspOuterClass;
|
||||
|
||||
public class PacketSetWidgetSlotRsp extends BasePacket {
|
||||
|
||||
public PacketSetWidgetSlotRsp(int materialId) {
|
||||
super(PacketOpcodes.SetWidgetSlotRsp);
|
||||
|
||||
SetWidgetSlotRspOuterClass.SetWidgetSlotRsp proto = SetWidgetSlotRspOuterClass.SetWidgetSlotRsp.newBuilder()
|
||||
.setMaterialId(materialId)
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.RetcodeOuterClass;
|
||||
import emu.grasscutter.net.proto.TryEnterHomeRspOuterClass;
|
||||
|
||||
public class PacketTryEnterHomeRsp extends BasePacket {
|
||||
|
||||
public PacketTryEnterHomeRsp() {
|
||||
super(PacketOpcodes.TryEnterHomeRsp);
|
||||
|
||||
TryEnterHomeRspOuterClass.TryEnterHomeRsp proto = TryEnterHomeRspOuterClass.TryEnterHomeRsp.newBuilder()
|
||||
.setRetcode(RetcodeOuterClass.Retcode.RET_SVR_ERROR_VALUE)
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
|
||||
public PacketTryEnterHomeRsp(int uid) {
|
||||
super(PacketOpcodes.TryEnterHomeRsp);
|
||||
|
||||
TryEnterHomeRspOuterClass.TryEnterHomeRsp proto = TryEnterHomeRspOuterClass.TryEnterHomeRsp.newBuilder()
|
||||
.setRetcode(0)
|
||||
.setTargetUid(uid)
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.VehicleStaminaNotifyOuterClass.VehicleStaminaNotify;
|
||||
|
||||
public class PacketVehicleStaminaNotify extends BasePacket {
|
||||
|
||||
public PacketVehicleStaminaNotify(int vehicleId, float newStamina) {
|
||||
super(PacketOpcodes.VehicleStaminaNotify);
|
||||
VehicleStaminaNotify.Builder proto = VehicleStaminaNotify.newBuilder();
|
||||
|
||||
proto.setEntityId(vehicleId);
|
||||
proto.setCurStamina(newStamina);
|
||||
|
||||
this.setData(proto.build());
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.WidgetCoolDownDataOuterClass;
|
||||
import emu.grasscutter.net.proto.WidgetCoolDownNotifyOuterClass;
|
||||
|
||||
public class PacketWidgetCoolDownNotify extends BasePacket {
|
||||
|
||||
public PacketWidgetCoolDownNotify(int id, long coolDownTime, boolean isSuccess) {
|
||||
super(PacketOpcodes.WidgetCoolDownNotify);
|
||||
|
||||
WidgetCoolDownNotifyOuterClass.WidgetCoolDownNotify proto = WidgetCoolDownNotifyOuterClass.WidgetCoolDownNotify.newBuilder()
|
||||
.addGroupCoolDownDataList(
|
||||
WidgetCoolDownDataOuterClass.WidgetCoolDownData.newBuilder()
|
||||
.setId(id)
|
||||
.setCoolDownTime(coolDownTime)
|
||||
.setIsSuccess(isSuccess)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.BasePacket;
|
||||
import emu.grasscutter.net.packet.PacketOpcodes;
|
||||
import emu.grasscutter.net.proto.WidgetDoBagRspOuterClass;
|
||||
|
||||
public class PacketWidgetDoBagRsp extends BasePacket {
|
||||
|
||||
public PacketWidgetDoBagRsp(int materialId) {
|
||||
super(PacketOpcodes.WidgetDoBagRsp);
|
||||
|
||||
WidgetDoBagRspOuterClass.WidgetDoBagRsp proto = WidgetDoBagRspOuterClass.WidgetDoBagRsp.newBuilder()
|
||||
.setMaterialId(materialId)
|
||||
.setRetcode(0)
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
|
||||
public PacketWidgetDoBagRsp() {
|
||||
super(PacketOpcodes.WidgetDoBagRsp);
|
||||
|
||||
WidgetDoBagRspOuterClass.WidgetDoBagRsp proto = WidgetDoBagRspOuterClass.WidgetDoBagRsp.newBuilder()
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user