Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
ziqian zhang
Grasscutter
Commits
a2ff8c84
Commit
a2ff8c84
authored
May 14, 2022
by
KingRainbow44
Browse files
Merge `development` into `plugin-auth`
parents
3adf0d44
a751e71d
Changes
111
Hide whitespace changes
Inline
Side-by-side
src/main/java/emu/grasscutter/game/quest/handlers/QuestBaseHandler.java
0 → 100644
View file @
a2ff8c84
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
);
}
src/main/java/emu/grasscutter/game/tower/TowerScheduleManager.java
View file @
a2ff8c84
...
...
@@ -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
;
...
...
src/main/java/emu/grasscutter/game/world/World.java
View file @
a2ff8c84
...
...
@@ -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
...
...
src/main/java/emu/grasscutter/server/game/GameServer.java
View file @
a2ff8c84
...
...
@@ -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
;
}
...
...
src/main/java/emu/grasscutter/server/game/GameSession.java
View file @
a2ff8c84
...
...
@@ -252,6 +252,7 @@ public class GameSession extends KcpChannel {
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
}
finally
{
data
.
release
();
packet
.
release
();
}
}
...
...
src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java
View file @
a2ff8c84
...
...
@@ -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
.
get
RegionInfo
().
to
Builder
()
var
regionInfo
=
RegionInfo
.
new
Builder
()
.
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
.
to
Builder
().
setRegionInfo
(
regionInfo
).
build
();
var
updatedQuery
=
QueryCurrRegionHttpRsp
.
new
Builder
().
setRegionInfo
(
regionInfo
).
build
();
regions
.
put
(
region
.
Name
,
new
RegionData
(
updatedQuery
,
Utils
.
base64Encode
(
updatedQuery
.
toByteString
().
toByteArray
())));
});
//
Decode the initial region lis
t.
byte
[]
listBase64
=
Base64
.
getDecoder
().
decode
(
this
.
regionList
);
QueryRegionListHttpRsp
regionList
=
QueryRegionListHttpRsp
.
parseFrom
(
listBase64
);
//
Create a config objec
t.
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
.
getClientC
ustomConfig
Encrypted
(
))
.
setClientSecretKey
(
ByteString
.
copyFrom
(
Crypto
.
DISPATCH_SEED
))
.
setClientCustomConfigEncrypted
(
ByteString
.
copyFrom
(
c
ustomConfig
))
.
setEnableLoginPc
(
true
).
build
();
// Set the region list response.
...
...
src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java
View file @
a2ff8c84
...
...
@@ -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
));
}
}
}
}
...
...
src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java
View file @
a2ff8c84
...
...
@@ -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
(
frontend
Template
response
.
send
(
records
Template
.
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
);
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java
View file @
a2ff8c84
...
...
@@ -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
()));
...
...
src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTransPointRegionNotify.java
View file @
a2ff8c84
...
...
@@ -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
();
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerExitTransPointRegionNotify.java
View file @
a2ff8c84
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
();
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerGetWidgetSlotReq.java
View file @
a2ff8c84
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
));
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeChooseModuleReq.java
0 → 100644
View file @
a2ff8c84
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
()));
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerMarkMapReq.java
View file @
a2ff8c84
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
);
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerNpcTalkReq.java
View file @
a2ff8c84
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
()));
}
...
...
src/main/java/emu/grasscutter/server/packet/recv/HandlerSetWidgetSlotReq.java
0 → 100644
View file @
a2ff8c84
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
()));
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerTryEnterHomeReq.java
0 → 100644
View file @
a2ff8c84
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
()));
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerVehicleInteractReq.java
View file @
a2ff8c84
...
...
@@ -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
()));
}
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerWidgetDoBagReq.java
0 → 100644
View file @
a2ff8c84
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
());
}
}
}
}
src/main/java/emu/grasscutter/server/packet/send/PacketAllWidgetDataNotify.java
0 → 100644
View file @
a2ff8c84
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
);
}
}
Prev
1
2
3
4
5
6
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment