mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
update
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
[OnlineSubsystemMcp.Xmpp]
|
||||
bUseSSL=false
|
||||
ServerAddr="ws://lawinserverxmpp.herokuapp.com"
|
||||
ServerPort=80
|
||||
|
||||
[OnlineSubsystemMcp.Xmpp Prod]
|
||||
bUseSSL=false
|
||||
ServerAddr="ws://lawinserverxmpp.herokuapp.com"
|
||||
ServerPort=80
|
||||
|
||||
[OnlineSubsystemMcp]
|
||||
bUsePartySystemV2=false
|
||||
|
||||
[OnlineSubsystemMcp.OnlinePartySystemMcpAdapter]
|
||||
bUsePartySystemV2=false
|
||||
Binary file not shown.
BIN
assets/binaries/craniumv2.dll
Normal file
BIN
assets/binaries/craniumv2.dll
Normal file
Binary file not shown.
BIN
assets/binaries/leakv2.dll
Normal file
BIN
assets/binaries/leakv2.dll
Normal file
Binary file not shown.
@@ -1 +1,2 @@
|
||||
for /f "tokens=5" %%a in ('netstat -aon ^| find ":3551" ^| find "LISTENING"') do taskkill /f /pid %%a
|
||||
for /f "tokens=5" %%a in ('netstat -aon ^| find ":8080" ^| find "LISTENING"') do taskkill /f /pid %%a
|
||||
1
assets/binaries/service.bat
Normal file
1
assets/binaries/service.bat
Normal file
@@ -0,0 +1 @@
|
||||
sc query "MongoDB" | findstr /i "STATE"
|
||||
17
assets/config/DefaultEngine.ini
Normal file
17
assets/config/DefaultEngine.ini
Normal file
@@ -0,0 +1,17 @@
|
||||
[ConsoleVariables]
|
||||
FortMatchmakingV2.EnableContentBeacon=0
|
||||
FortMatchmakingV2.ContentBeaconFailureCancelsMatchmaking=0
|
||||
|
||||
[PatchCheck]
|
||||
ModuleName=FortnitePatchCheck
|
||||
bCheckPlatformOSSForUpdate=false
|
||||
bCheckOSSForUpdate=false
|
||||
|
||||
[XMPP]
|
||||
bEnableWebsockets=false
|
||||
|
||||
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp
|
||||
[OnlineSubsystemMcp.Xmpp Prod]
|
||||
bUseSSL=false
|
||||
ServerAddr="ws://127.0.0.1"
|
||||
ServerPort=80
|
||||
28
assets/config/DefaultGame.ini
Normal file
28
assets/config/DefaultGame.ini
Normal file
@@ -0,0 +1,28 @@
|
||||
[/Script/FortniteGame.FortGlobals]
|
||||
bAthenaLeaderboardFrontEndEnabled=false
|
||||
bAthenaStatsFrontendEnabled=false
|
||||
bGlobalLeaderboardsFrontEndEnabled=false
|
||||
SubGameAccess=(SubGame=Campaign,AccessStatus=OpenAccess,MatchmakingStatus=Enabled)
|
||||
+SubGameAccess=(SubGame=Athena,AccessStatus=OpenAccess,MatchmakingStatus=Enabled)
|
||||
+SubGameAccess=(SubGame=Campaign,AccessStatus=OpenAccess,MatchmakingStatus=Enabled)
|
||||
bUploadAthenaStats=false
|
||||
bUploadAthenaStatsV2=false
|
||||
|
||||
[/Script/FortniteGame.FortMatchmakingV2]
|
||||
bCustomKeyEnabled=false
|
||||
|
||||
[/Script/FortniteGame.FortChatManager]
|
||||
bShouldRequestGeneralChatRooms=false
|
||||
bShouldJoinGlobalChat=false
|
||||
bShouldJoinFounderChat=false
|
||||
bIsAthenaGlobalChatEnabled=false
|
||||
|
||||
[/Script/FortniteGame.FortGameInstance]
|
||||
!FrontEndPlaylistData=ClearArray
|
||||
bBattleRoyaleMatchmakingEnabled=true
|
||||
+FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSolo, PlaylistAccess=(bEnabled=True, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=0, bDisplayAsLimitedTime=false, DisplayPriority=3))
|
||||
+FrontEndPlaylistData=(PlaylistName=Playlist_DefaultDuo, PlaylistAccess=(bEnabled=True, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=0, bDisplayAsLimitedTime=false, DisplayPriority=4))
|
||||
+FrontEndPlaylistData=(PlaylistName=Playlist_Trios, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=False, bDisplayAsLimitedTime=false, DisplayPriority=5, CategoryIndex=0))
|
||||
+FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSquad, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=0, bDisplayAsLimitedTime=false, DisplayPriority=6))
|
||||
+FrontEndPlaylistData=(PlaylistName=Playlist_PlaygroundV2, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=false, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=2, bDisplayAsLimitedTime=false, DisplayPriority=16))
|
||||
+FrontEndPlaylistData=(PlaylistName=Playlist_Campaign, PlaylistAccess=(bEnabled=true, bInvisibleWhenEnabled=true))
|
||||
18
assets/config/DefaultRuntimeOptions.ini
Normal file
18
assets/config/DefaultRuntimeOptions.ini
Normal file
@@ -0,0 +1,18 @@
|
||||
[/Script/FortniteGame.FortRuntimeOptions]
|
||||
bEnableGlobalChat=false
|
||||
bEnableMexiCola=false
|
||||
bLoadDirectlyIntoLobby=true
|
||||
bEnableSocialTab=false
|
||||
bMOTDSameNewsForCreative=true
|
||||
bForceBRMode=true
|
||||
bEnableSavedLoadouts=false
|
||||
bIsOutOfSeasonMode=true
|
||||
!DisabledTabsForOutOfSeason=ClearArray
|
||||
+DisabledTabsForOutOfSeason=(TabName="Lobby",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="AthenaCompete",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="AthenaCareer",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="AthenaStore",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="CareerScreen",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="AthenaDirectAcquisition",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="BattlePass",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="AthenaCustomize",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
BIN
assets/icons/reboot.ico
Normal file
BIN
assets/icons/reboot.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
103315
assets/profiles/athena.json
Normal file
103315
assets/profiles/athena.json
Normal file
File diff suppressed because it is too large
Load Diff
63511
assets/profiles/campaign.json
Normal file
63511
assets/profiles/campaign.json
Normal file
File diff suppressed because it is too large
Load Diff
178
assets/profiles/collection_book_people0.json
Normal file
178
assets/profiles/collection_book_people0.json
Normal file
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"_id": "LawinServer",
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "collection_book_people0",
|
||||
"version": "no_version",
|
||||
"items": {
|
||||
"CollectionBookPage:pageHeroes_Commando": {
|
||||
"templateId": "CollectionBookPage:pageHeroes_Commando",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageHeroes_Constructor": {
|
||||
"templateId": "CollectionBookPage:pageHeroes_Constructor",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageHeroes_Ninja": {
|
||||
"templateId": "CollectionBookPage:pageHeroes_Ninja",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageHeroes_Outlander": {
|
||||
"templateId": "CollectionBookPage:pageHeroes_Outlander",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pagePeople_Defenders": {
|
||||
"templateId": "CollectionBookPage:pagePeople_Defenders",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pagePeople_Survivors": {
|
||||
"templateId": "CollectionBookPage:pagePeople_Survivors",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pagePeople_Leads": {
|
||||
"templateId": "CollectionBookPage:pagePeople_Leads",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pagePeople_UniqueLeads": {
|
||||
"templateId": "CollectionBookPage:pagePeople_UniqueLeads",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Winter2017_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Winter2017_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Halloween2017_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Halloween2017_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Halloween2017_Workers": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Halloween2017_Workers",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_ChineseNewYear2018_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_ChineseNewYear2018_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_SpringItOn2018_People": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_SpringItOn2018_People",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_StormZoneCyber_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_StormZoneCyber_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Blockbuster2018_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Blockbuster2018_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_ShadowOps_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_ShadowOps_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_RoadTrip2018_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_RoadTrip2018_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_WildWest_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_WildWest_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_StormZone_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_StormZone_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Scavenger_Heroes": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Scavenger_Heroes",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"attributes": {
|
||||
"inventory_limit_bonus": 0
|
||||
}
|
||||
},
|
||||
"commandRevision": 0
|
||||
}
|
||||
458
assets/profiles/collection_book_schematics0.json
Normal file
458
assets/profiles/collection_book_schematics0.json
Normal file
@@ -0,0 +1,458 @@
|
||||
{
|
||||
"_id": "LawinServer",
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "collection_book_schematics0",
|
||||
"version": "no_version",
|
||||
"items": {
|
||||
"CollectionBookPage:pageMelee_Axes_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Axes_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Axes_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Axes_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Clubs_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Clubs_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Clubs_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Clubs_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Scythes_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Scythes_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Scythes_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Scythes_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Spears_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Spears_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Spears_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Spears_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Swords_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Swords_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Swords_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Swords_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Tools_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Tools_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageMelee_Tools_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageMelee_Tools_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageRanged_Assault_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageRanged_Assault_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageRanged_Assault_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageRanged_Assault_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageRanged_Shotgun_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageRanged_Shotgun_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageRanged_Shotgun_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageRanged_Shotgun_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:page_Ranged_Pistols_Weapons": {
|
||||
"templateId": "CollectionBookPage:page_Ranged_Pistols_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:page_Ranged_Pistols_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:page_Ranged_Pistols_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageRanged_Snipers_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageRanged_Snipers_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageRanged_Snipers_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:pageRanged_Snipers_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageRanged_Explosive_Weapons": {
|
||||
"templateId": "CollectionBookPage:pageRanged_Explosive_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageTraps_Wall": {
|
||||
"templateId": "CollectionBookPage:pageTraps_Wall",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageTraps_Ceiling": {
|
||||
"templateId": "CollectionBookPage:pageTraps_Ceiling",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:pageTraps_Floor": {
|
||||
"templateId": "CollectionBookPage:pageTraps_Floor",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Ranged_Medieval": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Ranged_Medieval",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Ranged_Medieval_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Ranged_Medieval_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Melee_Medieval": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Melee_Medieval",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Melee_Medieval_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Melee_Medieval_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Winter2017_Weapons": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Winter2017_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Winter2017_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Winter2017_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_RatRod_Weapons": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_RatRod_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_RatRod_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_RatRod_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Ranged_Winter2017": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Ranged_Winter2017",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Ranged_Winter2017_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Ranged_Winter2017_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Melee_Winter2017": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Melee_Winter2017",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Melee_Winter2017_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Melee_Winter2017_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_ChineseNewYear2018": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_ChineseNewYear2018",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Crystal_ChineseNewYear2018": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Crystal_ChineseNewYear2018",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_StormZoneCyber_Ranged": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_StormZoneCyber_Ranged",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_StormZoneCyber_Melee": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_StormZoneCyber_Melee",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_StormZoneCyber_Ranged_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_StormZoneCyber_Ranged_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_StormZoneCyber_Melee_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_StormZoneCyber_Melee_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Blockbuster2018_Ranged": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Blockbuster2018_Ranged",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Blockbuster2018_Ranged_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Blockbuster2018_Ranged_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_RoadTrip2018_Weapons": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_RoadTrip2018_Weapons",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_RoadTrip2018_Weapons_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_RoadTrip2018_Weapons_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Ranged_StormZone2": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Ranged_StormZone2",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Ranged_StormZone2_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Ranged_StormZone2_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Melee_StormZone2": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Melee_StormZone2",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Weapons_Melee_StormZone2_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Weapons_Melee_StormZone2_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Hydraulic": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Hydraulic",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Hydraulic_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Hydraulic_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Scavenger": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Scavenger",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:PageSpecial_Scavenger_Crystal": {
|
||||
"templateId": "CollectionBookPage:PageSpecial_Scavenger_Crystal",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"CollectionBookPage:test_TestPage": {
|
||||
"templateId": "CollectionBookPage:test_TestPage",
|
||||
"attributes": {
|
||||
"sectionStates": [],
|
||||
"state": "Active"
|
||||
},
|
||||
"quantity": 1
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"attributes": {
|
||||
"inventory_limit_bonus": 0
|
||||
}
|
||||
},
|
||||
"commandRevision": 0
|
||||
}
|
||||
15
assets/profiles/collections.json
Normal file
15
assets/profiles/collections.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"_id": "LawinServer",
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "collections",
|
||||
"version": "no_version",
|
||||
"items": {},
|
||||
"stats": {
|
||||
"attributes": {}
|
||||
},
|
||||
"commandRevision": 0
|
||||
}
|
||||
2204
assets/profiles/common_core.json
Normal file
2204
assets/profiles/common_core.json
Normal file
File diff suppressed because it is too large
Load Diff
18
assets/profiles/common_public.json
Normal file
18
assets/profiles/common_public.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "common_public",
|
||||
"version": "no_version",
|
||||
"items": {},
|
||||
"stats": {
|
||||
"attributes": {
|
||||
"banner_color": "DefaultColor15",
|
||||
"homebase_name": "",
|
||||
"banner_icon": "SurvivalBannerStonewoodComplete"
|
||||
}
|
||||
},
|
||||
"commandRevision": 0
|
||||
}
|
||||
15
assets/profiles/creative.json
Normal file
15
assets/profiles/creative.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"_id": "LawinServer",
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "creative",
|
||||
"version": "no_version",
|
||||
"items": {},
|
||||
"stats": {
|
||||
"attributes": {}
|
||||
},
|
||||
"commandRevision": 0
|
||||
}
|
||||
231
assets/profiles/metadata.json
Normal file
231
assets/profiles/metadata.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"_id": "LawinServer",
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "metadata",
|
||||
"version": "no_version",
|
||||
"items": {
|
||||
"Outpost:outpostcore_pve_03": {
|
||||
"templateId": "Outpost:outpostcore_pve_03",
|
||||
"attributes": {
|
||||
"cloud_save_info": {
|
||||
"saveCount": 319,
|
||||
"savedRecords": [
|
||||
{
|
||||
"recordIndex": 0,
|
||||
"archiveNumber": 1,
|
||||
"recordFilename": "eb192023-7db8-4bc0-b3e4-bf060c7baf87_r0_a1.sav"
|
||||
}
|
||||
]
|
||||
},
|
||||
"level": 10,
|
||||
"outpost_core_info": {
|
||||
"placedBuildings": [
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.00",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.01"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.01",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.00"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.02",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.05"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.03",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.02"
|
||||
}
|
||||
],
|
||||
"accountsWithEditPermission": [],
|
||||
"highestEnduranceWaveReached": 30
|
||||
}
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"Outpost:outpostcore_pve_02": {
|
||||
"templateId": "Outpost:outpostcore_pve_02",
|
||||
"attributes": {
|
||||
"cloud_save_info": {
|
||||
"saveCount": 603,
|
||||
"savedRecords": [
|
||||
{
|
||||
"recordIndex": 0,
|
||||
"archiveNumber": 0,
|
||||
"recordFilename": "76fe0295-aee2-463a-9229-d9933b4969b8_r0_a0.sav"
|
||||
}
|
||||
]
|
||||
},
|
||||
"level": 10,
|
||||
"outpost_core_info": {
|
||||
"placedBuildings": [
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.00",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.00"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.01",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.01"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.02",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.04"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.03",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.03"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.04",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.02"
|
||||
}
|
||||
],
|
||||
"accountsWithEditPermission": [],
|
||||
"highestEnduranceWaveReached": 30
|
||||
}
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"Outpost:outpostcore_pve_04": {
|
||||
"templateId": "Outpost:outpostcore_pve_04",
|
||||
"attributes": {
|
||||
"cloud_save_info": {
|
||||
"saveCount": 77,
|
||||
"savedRecords": [
|
||||
{
|
||||
"recordIndex": 0,
|
||||
"archiveNumber": 1,
|
||||
"recordFilename": "940037e4-87d2-499e-8d00-cdb2dfa326b9_r0_a1.sav"
|
||||
}
|
||||
]
|
||||
},
|
||||
"level": 10,
|
||||
"outpost_core_info": {
|
||||
"placedBuildings": [
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.00",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.00"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.01",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.01"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.02",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.03"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.03",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.05"
|
||||
}
|
||||
],
|
||||
"accountsWithEditPermission": [],
|
||||
"highestEnduranceWaveReached": 30
|
||||
}
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"Outpost:outpostcore_pve_01": {
|
||||
"templateId": "Outpost:outpostcore_pve_01",
|
||||
"attributes": {
|
||||
"cloud_save_info": {
|
||||
"saveCount": 851,
|
||||
"savedRecords": [
|
||||
{
|
||||
"recordIndex": 0,
|
||||
"archiveNumber": 0,
|
||||
"recordFilename": "a1d68ce6-63a5-499a-946f-9e0c825572d7_r0_a0.sav"
|
||||
}
|
||||
]
|
||||
},
|
||||
"level": 10,
|
||||
"outpost_core_info": {
|
||||
"placedBuildings": [
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.00",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.00"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.01",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.02"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.02",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.01"
|
||||
},
|
||||
{
|
||||
"buildingTag": "Outpost.BuildingActor.Building.03",
|
||||
"placedTag": "Outpost.PlacementActor.Placement.05"
|
||||
}
|
||||
],
|
||||
"accountsWithEditPermission": [],
|
||||
"highestEnduranceWaveReached": 30
|
||||
}
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"DeployableBaseCloudSave:testdeployablebaseitemdef": {
|
||||
"templateId": "DeployableBaseCloudSave:testdeployablebaseitemdef",
|
||||
"attributes": {
|
||||
"tier_progression": {
|
||||
"progressionInfo": [
|
||||
{
|
||||
"progressionLayoutGuid": "B70B5C69-437E-75C5-CB91-7E913F3B5294",
|
||||
"highestDefeatedTier": 0
|
||||
},
|
||||
{
|
||||
"progressionLayoutGuid": "04FD086F-4A99-823B-06C3-979A8F408960",
|
||||
"highestDefeatedTier": 4
|
||||
},
|
||||
{
|
||||
"progressionLayoutGuid": "D3D31F40-45D8-FD77-67E6-5FBAB0550417",
|
||||
"highestDefeatedTier": 1
|
||||
},
|
||||
{
|
||||
"progressionLayoutGuid": "92A17A43-4EDC-8F69-688F-24BB3A3D8AEF",
|
||||
"highestDefeatedTier": 3
|
||||
},
|
||||
{
|
||||
"progressionLayoutGuid": "A2D8DB3E-457E-279B-58F5-AA9BA2FDC547",
|
||||
"highestDefeatedTier": 4
|
||||
},
|
||||
{
|
||||
"progressionLayoutGuid": "5AAB9A15-49F5-0D74-0B22-BB9686396E8F",
|
||||
"highestDefeatedTier": 1
|
||||
},
|
||||
{
|
||||
"progressionLayoutGuid": "9077163A-4664-1993-5A20-D28170404FD6",
|
||||
"highestDefeatedTier": 3
|
||||
},
|
||||
{
|
||||
"progressionLayoutGuid": "FB679125-49BC-0025-48F3-22A1B8085189",
|
||||
"highestDefeatedTier": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"cloud_save_info": {
|
||||
"saveCount": 11,
|
||||
"savedRecords": [
|
||||
{
|
||||
"recordIndex": 0,
|
||||
"archiveNumber": 1,
|
||||
"recordFilename": "2FA8CFBB-4973-CCF0-EEA8-BEBC37D99F52_r0_a1.sav"
|
||||
}
|
||||
]
|
||||
},
|
||||
"level": 0
|
||||
},
|
||||
"quantity": 1
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"attributes": {
|
||||
"inventory_limit_bonus": 0
|
||||
}
|
||||
},
|
||||
"commandRevision": 0
|
||||
}
|
||||
17
assets/profiles/outpost0.json
Normal file
17
assets/profiles/outpost0.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"_id": "LawinServer",
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "outpost0",
|
||||
"version": "no_version",
|
||||
"items": {},
|
||||
"stats": {
|
||||
"attributes": {
|
||||
"inventory_limit_bonus": 0
|
||||
}
|
||||
},
|
||||
"commandRevision": 0
|
||||
}
|
||||
35978
assets/profiles/profile0.json
Normal file
35978
assets/profiles/profile0.json
Normal file
File diff suppressed because it is too large
Load Diff
694
assets/profiles/theater0.json
Normal file
694
assets/profiles/theater0.json
Normal file
@@ -0,0 +1,694 @@
|
||||
{
|
||||
"_id": "LawinServer",
|
||||
"created": "0001-01-01T00:00:00.000Z",
|
||||
"updated": "0001-01-01T00:00:00.000Z",
|
||||
"rvn": 0,
|
||||
"wipeNumber": 1,
|
||||
"accountId": "LawinServer",
|
||||
"profileId": "theater0",
|
||||
"version": "no_version",
|
||||
"items": {
|
||||
"3d81f6f3-1290-326e-dfee-e577af2e9fbb": {
|
||||
"templateId": "Ingredient:ingredient_blastpowder",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"70ff3716-d732-c472-b1d8-0a20d48dd607": {
|
||||
"templateId": "Ingredient:ingredient_ore_silver",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"48059439-88b0-a779-daae-36d9495f079e": {
|
||||
"templateId": "Ingredient:ingredient_ore_alloy",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"7dd4a423-0b6f-3abb-757c-88077adcaacc": {
|
||||
"templateId": "Ingredient:ingredient_crystal_sunbeam",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"694a4c8d-67b6-f903-85bf-f33d4e7a6859": {
|
||||
"templateId": "Ingredient:ingredient_ore_obsidian",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"97feb4c9-2290-fd4b-c356-7f346ba67e39": {
|
||||
"templateId": "Ingredient:ingredient_mechanical_parts_t05",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"25ff4168-8eb9-a5cc-4900-d06cdb8004ab": {
|
||||
"templateId": "Ingredient:ingredient_mechanical_parts_t02",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"a63022b8-6467-a347-7c11-37d483d45d08": {
|
||||
"templateId": "Ingredient:ingredient_rare_powercell",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"7e0d23d2-24dc-9579-4f32-507758107bd3": {
|
||||
"templateId": "Ingredient:ingredient_resin",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"d44ad9ed-a5d3-0642-a865-083061aeb4e6": {
|
||||
"templateId": "Ingredient:ingredient_powder_t05",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"2011030e-dbce-a02b-c086-ec8a99f16aeb": {
|
||||
"templateId": "Ingredient:ingredient_crystal_quartz",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"7d43d569-e170-bd46-dfb2-92828ff0c98d": {
|
||||
"templateId": "Ingredient:ingredient_rare_mechanism",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"1db23fbf-1ab5-72d9-2b20-50eacebda6d5": {
|
||||
"templateId": "Ingredient:ingredient_nuts_bolts",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 0,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"024aa359-e313-168a-3738-31ae4f04cfb2": {
|
||||
"templateId": "Ingredient:ingredient_ore_brightcore",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"da7680a3-072e-3e3f-3ed6-bf71159f6df0": {
|
||||
"templateId": "Ingredient:ingredient_planks",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"10aec620-f4b9-aadd-da3e-5d4a8f87225b": {
|
||||
"templateId": "Ingredient:ingredient_ore_malachite",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"161217ac-5b39-a93e-a1d8-7f7667646624": {
|
||||
"templateId": "Ingredient:ingredient_crystal_shadowshard",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"f33663f5-bf16-9315-8df2-91800944b3e8": {
|
||||
"templateId": "Ingredient:ingredient_twine_t05",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"aa4a3cce-9bb5-50ef-4c59-f959e89c3992": {
|
||||
"templateId": "Ingredient:ingredient_twine_t01",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"e9eca1e1-7665-1315-de97-6583394e0af1": {
|
||||
"templateId": "Ingredient:ingredient_batteries",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"d1fd9cb3-0d51-6d7c-e937-9b07406ba42e": {
|
||||
"templateId": "Ingredient:ingredient_twine_t04",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"7d8f824d-cec2-01d5-0efc-c30073402de2": {
|
||||
"templateId": "Ingredient:ingredient_herbs",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"bdb45648-18f6-fdc6-8252-b717043f0021": {
|
||||
"templateId": "Ingredient:ingredient_mechanical_parts_t04",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"ddc2382e-faf9-14dd-c721-c659660540a8": {
|
||||
"templateId": "Ingredient:ingredient_ore_copper",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"75582a66-bc3e-958a-1943-79a56150d0bb": {
|
||||
"templateId": "Ingredient:ingredient_powder_t03",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"b4616de1-caf4-3652-a613-edb932df71e0": {
|
||||
"templateId": "Ingredient:ingredient_mechanical_parts_t01",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"bdc338a8-3667-e7e4-280d-5d4e4255b3f1": {
|
||||
"templateId": "Ingredient:ingredient_twine_t02",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"d17582f7-eb63-a4a6-cd4d-ff3d68e69757": {
|
||||
"templateId": "Ingredient:ingredient_powder_t01",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"25e43cee-7dc7-348b-40bc-20b8850468ba": {
|
||||
"templateId": "Ingredient:ingredient_duct_tape",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"720bc675-44e2-ff74-6e5c-eec23b493bd1": {
|
||||
"templateId": "Ingredient:ingredient_twine_t03",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"0e2094f2-9c35-9e51-58ea-a87ec89fa758": {
|
||||
"templateId": "Ingredient:ingredient_powder_t02",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"1cf850a0-1797-4fe8-dd94-34152756c80b": {
|
||||
"templateId": "Ingredient:ingredient_bacon",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 0,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"7fe47331-1cbd-4606-c12e-6df2c1dc13a3": {
|
||||
"templateId": "Ingredient:ingredient_mechanical_parts_t03",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"42d429fc-a4cf-974d-2bce-17c6b872c96e": {
|
||||
"templateId": "Ingredient:ingredient_ore_coal",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"124d77cc-8cd4-fdcc-efe1-c18ee63587eb": {
|
||||
"templateId": "Ingredient:ingredient_flowers",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"ea2a6495-4b9e-59df-0163-5e5e8f52467e": {
|
||||
"templateId": "Ingredient:ingredient_powder_t04",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"6291ab77-ec9b-1b35-ccb0-063519415f6d": {
|
||||
"templateId": "WorldItem:wooditemdata",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"2d7953c0-752f-c2a7-ebef-90b45cb30b5b": {
|
||||
"templateId": "WorldItem:stoneitemdata",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"06f471d5-046b-50f6-3f07-9aa670b6fecb": {
|
||||
"templateId": "WorldItem:metalitemdata",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"003e7a8c-92eb-13c1-6b0e-aafad8f3d81d": {
|
||||
"templateId": "Weapon:edittool",
|
||||
"attributes": {
|
||||
"clipSizeScale": 0,
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"baseClipSize": 0,
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"bcb13e35-c030-cf2a-a003-16377320beda": {
|
||||
"templateId": "Weapon:buildingitemdata_wall",
|
||||
"attributes": {
|
||||
"clipSizeScale": 0,
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"baseClipSize": 0,
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"97ba026b-a36c-6827-e9d7-21bc6a1f9c53": {
|
||||
"templateId": "Weapon:buildingitemdata_floor",
|
||||
"attributes": {
|
||||
"clipSizeScale": 0,
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"baseClipSize": 0,
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"15c02d16-11f6-ffd1-e8bb-4b5d56bd5bd9": {
|
||||
"templateId": "Weapon:buildingitemdata_stair_w",
|
||||
"attributes": {
|
||||
"clipSizeScale": 0,
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"baseClipSize": 0,
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"f603c8af-e326-202e-3b12-b5fd2517e5c2": {
|
||||
"templateId": "Weapon:buildingitemdata_roofs",
|
||||
"attributes": {
|
||||
"clipSizeScale": 0,
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"baseClipSize": 0,
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 1
|
||||
},
|
||||
"baa4f86c-708c-7689-0859-fbfdb1bc623a": {
|
||||
"templateId": "Ammo:ammodatabulletsmedium",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"240894a2-f99a-214d-ce50-2dac39394699": {
|
||||
"templateId": "Ammo:ammodatashells",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": "None"
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"1a0c69f4-2c23-a5c9-34a0-f48c45637171": {
|
||||
"templateId": "Ammo:ammodataenergycell",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": ""
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"a92b1e7e-5812-0bfd-0107-f7b97ed166fa": {
|
||||
"templateId": "Ammo:ammodatabulletsheavy",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": "None"
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"7516967c-e831-4c3a-1a24-03834756f532": {
|
||||
"templateId": "Ammo:ammodatabulletslight",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": "None"
|
||||
},
|
||||
"quantity": 999
|
||||
},
|
||||
"23220ed0-29d4-3da1-6e0e-6df46a19752d": {
|
||||
"templateId": "Ammo:ammodataexplosive",
|
||||
"attributes": {
|
||||
"loadedAmmo": 0,
|
||||
"inventory_overflow_date": false,
|
||||
"level": 0,
|
||||
"alterationDefinitions": [],
|
||||
"durability": 1,
|
||||
"itemSource": "None"
|
||||
},
|
||||
"quantity": 999
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"attributes": {
|
||||
"player_loadout": {
|
||||
"bPlayerIsNew": false,
|
||||
"pinnedSchematicInstances": [],
|
||||
"primaryQuickBarRecord": {
|
||||
"slots": [
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"secondaryQuickBarRecord": {
|
||||
"slots": [
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
},
|
||||
{
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"zonesCompleted": 0
|
||||
},
|
||||
"theater_unique_id": "",
|
||||
"past_lifetime_zones_completed": 0,
|
||||
"last_event_instance_key": "",
|
||||
"last_zones_completed": 0,
|
||||
"inventory_limit_bonus": 0
|
||||
}
|
||||
},
|
||||
"profileLockExpiration": "0001-01-01T00:00:00.000Z",
|
||||
"commandRevision": 0
|
||||
}
|
||||
459
assets/responses/ItemIDS.json
Normal file
459
assets/responses/ItemIDS.json
Normal file
@@ -0,0 +1,459 @@
|
||||
[
|
||||
"Schematic:SID_Wall_Wood_Spikes_vR_T01",
|
||||
"Hero:HID_Commando_Sony_R_T01",
|
||||
"Schematic:SID_Wall_Wood_Spikes_UC_T01",
|
||||
"Hero:HID_Commando_ShockDamage_VR_T01",
|
||||
"Schematic:SID_Wall_Wood_Spikes_SR_T01",
|
||||
"Hero:HID_Commando_ShockDamage_SR_T01",
|
||||
"Schematic:SID_Wall_Wood_Spikes_R_T01",
|
||||
"Hero:HID_Commando_ShockDamage_R_T01",
|
||||
"Schematic:SID_Wall_Wood_Spikes_C_T01",
|
||||
"Hero:HID_Commando_GunTough_VR_T01",
|
||||
"Schematic:SID_Wall_Light_VR_T01",
|
||||
"Hero:HID_Commando_GunTough_UC_T01",
|
||||
"Schematic:SID_Wall_Light_SR_T01",
|
||||
"Hero:HID_Commando_GunTough_SR_T01",
|
||||
"Schematic:SID_Wall_Light_R_T01",
|
||||
"Hero:HID_Commando_GunTough_R_T01",
|
||||
"Schematic:SID_Wall_Launcher_VR_T01",
|
||||
"Hero:HID_Commando_GunHeadshotHW_SR_T01",
|
||||
"Schematic:SID_Wall_Launcher_UC_T01",
|
||||
"Hero:HID_Commando_GunHeadshot_VR_T01",
|
||||
"Schematic:SID_Wall_Launcher_SR_T01",
|
||||
"Hero:HID_Commando_GunHeadshot_SR_T01",
|
||||
"Schematic:SID_Wall_Launcher_R_T01",
|
||||
"Hero:HID_Commando_GrenadeMaster_SR_T01",
|
||||
"Schematic:SID_Wall_Electric_VR_T01",
|
||||
"Hero:HID_Commando_GrenadeGun_VR_T01",
|
||||
"Schematic:SID_Wall_Electric_UC_T01",
|
||||
"Hero:HID_Commando_GrenadeGun_SR_T01",
|
||||
"Schematic:SID_Wall_Electric_SR_T01",
|
||||
"Hero:HID_Commando_GrenadeGun_R_T01",
|
||||
"Schematic:SID_Wall_Electric_R_T01",
|
||||
"Hero:HID_Commando_GCGrenade_VR_T01",
|
||||
"Schematic:SID_Wall_Darts_VR_T01",
|
||||
"Hero:HID_Commando_GCGrenade_SR_T01",
|
||||
"Schematic:SID_Wall_Darts_UC_T01",
|
||||
"Hero:HID_Commando_GCGrenade_R_T01",
|
||||
"Schematic:SID_Wall_Darts_SR_T01",
|
||||
"Hero:HID_Commando_010_VR_T01",
|
||||
"Schematic:SID_Wall_Darts_R_T01",
|
||||
"Hero:HID_Commando_010_SR_T01",
|
||||
"Schematic:SID_Floor_Ward_VR_T01",
|
||||
"Hero:HID_Commando_009_VR_T01",
|
||||
"Schematic:SID_Floor_Ward_UC_T01",
|
||||
"Hero:HID_Commando_009_SR_T01",
|
||||
"Schematic:SID_Floor_Ward_SR_T01",
|
||||
"Hero:HID_Commando_009_R_T01",
|
||||
"Schematic:SID_Floor_Ward_R_T01",
|
||||
"Hero:HID_Commando_008_VR_T01",
|
||||
"Schematic:SID_Floor_Spikes_Wood_VR_T01",
|
||||
"Hero:HID_Commando_008_SR_T01",
|
||||
"Schematic:SID_Floor_Spikes_Wood_UC_T01",
|
||||
"Hero:HID_Commando_008_R_T01",
|
||||
"Schematic:SID_Floor_Spikes_Wood_SR_T01",
|
||||
"Hero:HID_Commando_008_FoundersM_SR_T01",
|
||||
"Schematic:SID_Floor_Spikes_Wood_R_T01",
|
||||
"Hero:HID_Commando_008_FoundersF_SR_T01",
|
||||
"Schematic:SID_Floor_Spikes_Wood_C_T01",
|
||||
"Hero:HID_Commando_007_VR_T01",
|
||||
"Schematic:SID_Floor_Spikes_VR_T01",
|
||||
"Hero:HID_Commando_007_UC_T01",
|
||||
"Schematic:SID_Floor_Spikes_UC_T01",
|
||||
"Hero:HID_Commando_007_SR_T01",
|
||||
"Schematic:SID_Floor_Spikes_SR_T01",
|
||||
"Hero:HID_Commando_007_R_T01",
|
||||
"Schematic:SID_Floor_Spikes_R_T01",
|
||||
"Hero:HID_Commando_GrenadeGun_UC_T01",
|
||||
"Schematic:SID_Floor_Launcher_VR_T01",
|
||||
"Hero:HID_Constructor_Sony_R_T01",
|
||||
"Schematic:SID_Floor_Launcher_UC_T01",
|
||||
"Hero:HID_Constructor_RushBASE_VR_T01",
|
||||
"Schematic:SID_Floor_Launcher_SR_T01",
|
||||
"Hero:HID_Constructor_RushBASE_UC_T01",
|
||||
"Schematic:SID_Floor_Launcher_R_T01",
|
||||
"Hero:HID_Constructor_RushBASE_SR_T01",
|
||||
"Schematic:SID_Floor_Health_VR_T01",
|
||||
"Hero:HID_Constructor_RushBASE_R_T01",
|
||||
"Schematic:SID_Floor_Health_UC_T01",
|
||||
"Hero:HID_Constructor_PlasmaDamage_VR_T01",
|
||||
"Schematic:SID_Floor_Health_SR_T01",
|
||||
"Hero:HID_Constructor_PlasmaDamage_SR_T01",
|
||||
"Schematic:SID_Floor_Health_R_T01",
|
||||
"Hero:HID_Constructor_PlasmaDamage_R_T01",
|
||||
"Schematic:SID_Ceiling_Gas_VR_T01",
|
||||
"Hero:HID_Constructor_HammerTank_VR_T01",
|
||||
"Schematic:SID_Ceiling_Gas_UC_T01",
|
||||
"Hero:HID_Constructor_HammerTank_UC_T01",
|
||||
"Schematic:SID_Ceiling_Gas_SR_T01",
|
||||
"Hero:HID_Constructor_HammerTank_SR_T01",
|
||||
"Schematic:SID_Ceiling_Gas_R_T01",
|
||||
"Hero:HID_Constructor_HammerTank_R_T01",
|
||||
"Schematic:SID_Ceiling_Electric_Single_VR_T01",
|
||||
"Hero:HID_Constructor_HammerPlasma_VR_T01",
|
||||
"Schematic:SID_Ceiling_Electric_Single_UC_T01",
|
||||
"Hero:HID_Constructor_HammerPlasma_SR_T01",
|
||||
"Schematic:SID_Ceiling_Electric_Single_SR_T01",
|
||||
"Hero:HID_Constructor_BaseHyperHW_SR_T01",
|
||||
"Schematic:SID_Ceiling_Electric_Single_R_T01",
|
||||
"Hero:HID_Constructor_BaseHyper_VR_T01",
|
||||
"Schematic:SID_Ceiling_Electric_Single_C_T01",
|
||||
"Hero:HID_Constructor_BaseHyper_SR_T01",
|
||||
"Schematic:SID_Ceiling_Electric_AOE_VR_T01",
|
||||
"Hero:HID_Constructor_BaseHyper_R_T01",
|
||||
"Schematic:SID_Ceiling_Electric_AOE_SR_T01",
|
||||
"Hero:HID_Constructor_BASEBig_SR_T01",
|
||||
"Schematic:SID_Ceiling_Electric_AOE_R_T01",
|
||||
"Hero:HID_Constructor_010_VR_T01",
|
||||
"Schematic:SID_Sniper_TripleShot_VR_Ore_T01",
|
||||
"Hero:HID_Constructor_010_SR_T01",
|
||||
"Schematic:SID_Sniper_TripleShot_SR_Ore_T01",
|
||||
"Hero:HID_Constructor_009_VR_T01",
|
||||
"Schematic:SID_Sniper_Standard_Scope_VR_Ore_T01",
|
||||
"Hero:HID_Constructor_009_SR_T01",
|
||||
"Schematic:SID_Sniper_Standard_Scope_SR_Ore_T01",
|
||||
"Hero:HID_Constructor_009_R_T01",
|
||||
"Schematic:SID_Sniper_Standard_VR_Ore_T01",
|
||||
"Hero:HID_Constructor_008_VR_T01",
|
||||
"Schematic:SID_Sniper_Standard_SR_Ore_T01",
|
||||
"Hero:HID_Constructor_008_SR_T01",
|
||||
"Schematic:SID_Sniper_Standard_Founders_VR_Ore_T01",
|
||||
"Hero:HID_Constructor_008_R_T01",
|
||||
"Schematic:SID_Sniper_Standard_UC_Ore_T01",
|
||||
"Hero:HID_Constructor_008_FoundersM_SR_T01",
|
||||
"Schematic:SID_Sniper_Standard_R_Ore_T01",
|
||||
"Hero:HID_Constructor_008_FoundersF_SR_T01",
|
||||
"Schematic:SID_Sniper_Standard_C_Ore_T01",
|
||||
"Hero:HID_Constructor_007_VR_T01",
|
||||
"Schematic:SID_Sniper_Shredder_VR_Ore_T01",
|
||||
"Hero:HID_Constructor_007_UC_T01",
|
||||
"Schematic:SID_Sniper_Shredder_SR_Ore_T01",
|
||||
"Hero:HID_Constructor_007_SR_T01",
|
||||
"Schematic:SID_Sniper_Hydraulic_VR_Ore_T01",
|
||||
"Hero:HID_Constructor_007_R_T01",
|
||||
"Schematic:SID_Sniper_Hydraulic_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_Swordmaster_SR_T01",
|
||||
"Schematic:SID_Sniper_BoltAction_Scope_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsRainHW_SR_T01",
|
||||
"Schematic:SID_Sniper_BoltAction_Scope_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsRain_VR_T01",
|
||||
"Schematic:SID_Sniper_BoltAction_Scope_R_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsRain_SR_T01",
|
||||
"Schematic:SID_Sniper_BoltAction_UC_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsAssassin_VR_T01",
|
||||
"Schematic:SID_Sniper_BoltAction_R_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsAssassin_UC_T01",
|
||||
"Schematic:SID_Sniper_BoltAction_C_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsAssassin_SR_T01",
|
||||
"Schematic:SID_Sniper_Auto_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsAssassin_R_T01",
|
||||
"Schematic:SID_Sniper_Auto_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsAssassin_FoundersM_SR_T01",
|
||||
"Schematic:SID_Sniper_Auto_Founders_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_StarsAssassin_FoundersF_SR_T01",
|
||||
"Schematic:SID_Sniper_Auto_UC_Ore_T01",
|
||||
"Hero:HID_Ninja_Sony_R_T01",
|
||||
"Schematic:SID_Sniper_Auto_R_Ore_T01",
|
||||
"Hero:HID_Ninja_SmokeDimMak_VR_T01",
|
||||
"Schematic:SID_Sniper_AMR_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_SmokeDimMak_SR_T01",
|
||||
"Schematic:SID_Sniper_AMR_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_SmokeDimMak_R_T01",
|
||||
"Schematic:SID_Sniper_AMR_R_Ore_T01",
|
||||
"Hero:HID_Ninja_SlashTail_VR_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_Precision_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_SlashTail_UC_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_Precision_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_SlashTail_SR_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_Precision_R_Ore_T01",
|
||||
"Hero:HID_Ninja_SlashTail_R_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_UC_Ore_T01",
|
||||
"Hero:HID_Ninja_SlashBreath_VR_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_R_Ore_T01",
|
||||
"Hero:HID_Ninja_SlashBreath_SR_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_Founders_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_SlashBreath_R_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_Founders_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_010_VR_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_Founders_R_Ore_T01",
|
||||
"Hero:HID_Ninja_010_SR_T01",
|
||||
"Schematic:SID_Shotgun_Tactical_C_Ore_T01",
|
||||
"Hero:HID_Ninja_009_VR_T01",
|
||||
"Schematic:SID_Shotgun_Standard_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_009_SR_T01",
|
||||
"Schematic:SID_Shotgun_Standard_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_009_R_T01",
|
||||
"Schematic:SID_Shotgun_Standard_UC_Ore_T01",
|
||||
"Hero:HID_Ninja_008_VR_T01",
|
||||
"Schematic:SID_Shotgun_Standard_R_Ore_T01",
|
||||
"Hero:HID_Ninja_008_SR_T01",
|
||||
"Schematic:SID_Shotgun_Standard_C_Ore_T01",
|
||||
"Hero:HID_Ninja_008_R_T01",
|
||||
"Schematic:SID_Shotgun_SemiAuto_VR_Ore_T01",
|
||||
"Hero:HID_Ninja_007_VR_T01",
|
||||
"Schematic:SID_Shotgun_SemiAuto_UC_Ore_T01",
|
||||
"Hero:HID_Ninja_007_UC_T01",
|
||||
"Schematic:SID_Shotgun_SemiAuto_SR_Ore_T01",
|
||||
"Hero:HID_Ninja_007_SR_T01",
|
||||
"Schematic:SID_Shotgun_SemiAuto_R_Ore_T01",
|
||||
"Hero:HID_Ninja_007_R_T01",
|
||||
"Schematic:SID_Shotgun_Minigun_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_ZonePistolHW_SR_T01",
|
||||
"Schematic:SID_Shotgun_Longarm_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_ZonePistol_VR_T01",
|
||||
"Schematic:SID_Shotgun_Longarm_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_ZonePistol_SR_T01",
|
||||
"Schematic:SID_Shotgun_Heavy_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_ZonePistol_R_T01",
|
||||
"Schematic:SID_Shotgun_Break_OU_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_ZoneHarvest_VR_T01",
|
||||
"Schematic:SID_Shotgun_Break_OU_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_ZoneHarvest_UC_T01",
|
||||
"Schematic:SID_Shotgun_Break_OU_UC_Ore_T01",
|
||||
"Hero:HID_Outlander_ZoneHarvest_SR_T01",
|
||||
"Schematic:SID_Shotgun_Break_OU_R_Ore_T01",
|
||||
"Hero:HID_Outlander_ZoneHarvest_R_T01",
|
||||
"Schematic:SID_Shotgun_Break_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_ZoneFragment_SR_T01",
|
||||
"Schematic:SID_Shotgun_Break_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_SphereFragment_VR_T01",
|
||||
"Schematic:SID_Shotgun_Break_UC_Ore_T01",
|
||||
"Hero:HID_Outlander_SphereFragment_SR_T01",
|
||||
"Schematic:SID_Shotgun_Break_R_Ore_T01",
|
||||
"Hero:HID_Outlander_SphereFragment_R_T01",
|
||||
"Schematic:SID_Shotgun_Break_C_Ore_T01",
|
||||
"Hero:HID_Outlander_Sony_R_T01",
|
||||
"Schematic:SID_Shotgun_Auto_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_PunchPhase_VR_T01",
|
||||
"Schematic:SID_Shotgun_Auto_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_PunchPhase_UC_T01",
|
||||
"Schematic:SID_Shotgun_Auto_Founders_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_PunchPhase_SR_T01",
|
||||
"Schematic:SID_Shotgun_Auto_UC_Ore_T01",
|
||||
"Hero:HID_Outlander_PunchPhase_R_T01",
|
||||
"Schematic:SID_Shotgun_Auto_R_Ore_T01",
|
||||
"Hero:HID_Outlander_PunchDamage_VR_T01",
|
||||
"Schematic:SID_Pistol_Zapper_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_PunchDamage_SR_T01",
|
||||
"Schematic:SID_Pistol_Zapper_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_010_VR_T01",
|
||||
"Schematic:SID_Pistol_Space_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_010_SR_T01",
|
||||
"Schematic:SID_Pistol_Space_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_009_VR_T01",
|
||||
"Schematic:SID_Pistol_SixShooter_UC_Ore_T01",
|
||||
"Hero:HID_Outlander_009_SR_T01",
|
||||
"Schematic:SID_Pistol_SixShooter_R_Ore_T01",
|
||||
"Hero:HID_Outlander_009_R_T01",
|
||||
"Schematic:SID_Pistol_SixShooter_C_Ore_T01",
|
||||
"Hero:HID_Outlander_008_VR_T01",
|
||||
"Schematic:SID_Pistol_SemiAuto_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_008_SR_T01",
|
||||
"Schematic:SID_Pistol_SemiAuto_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_008_R_T01",
|
||||
"Schematic:SID_Pistol_SemiAuto_Founders_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_008_FoundersM_SR_T01",
|
||||
"Schematic:SID_Pistol_SemiAuto_UC_Ore_T01",
|
||||
"Hero:HID_Outlander_008_FoundersF_SR_T01",
|
||||
"Schematic:SID_Pistol_SemiAuto_R_Ore_T01",
|
||||
"Hero:HID_Outlander_007_VR_T01",
|
||||
"Schematic:SID_Pistol_SemiAuto_C_Ore_T01",
|
||||
"Hero:HID_Outlander_007_UC_T01",
|
||||
"Schematic:SID_Pistol_Rocket_SR_Ore_T01",
|
||||
"Hero:HID_Outlander_007_SR_T01",
|
||||
"Schematic:SID_Pistol_Rapid_VR_Ore_T01",
|
||||
"Hero:HID_Outlander_007_R_T01",
|
||||
"Schematic:SID_Pistol_Rapid_SR_Ore_T01",
|
||||
"Defender:DID_DefenderSniper_Basic_VR_T01",
|
||||
"Schematic:SID_Pistol_Rapid_R_Ore_T01",
|
||||
"Defender:DID_DefenderSniper_Basic_UC_T01",
|
||||
"Schematic:SID_Pistol_Rapid_Founders_VR_Ore_T01",
|
||||
"Defender:DID_DefenderSniper_Basic_SR_T01",
|
||||
"Schematic:SID_Pistol_Hydraulic_VR_Ore_T01",
|
||||
"Defender:DID_DefenderSniper_Basic_R_T01",
|
||||
"Schematic:SID_Pistol_Hydraulic_SR_Ore_T01",
|
||||
"Defender:DID_DefenderSniper_Basic_C_T01",
|
||||
"Schematic:SID_Pistol_Handcannon_Semi_VR_Ore_T01",
|
||||
"Defender:DID_DefenderShotgun_Basic_VR_T01",
|
||||
"Schematic:SID_Pistol_Handcannon_Semi_SR_Ore_T01",
|
||||
"Defender:DID_DefenderShotgun_Basic_UC_T01",
|
||||
"Schematic:SID_Pistol_Handcannon_Semi_R_Ore_T01",
|
||||
"Defender:DID_DefenderShotgun_Basic_SR_T01",
|
||||
"Schematic:SID_Pistol_Handcannon_VR_Ore_T01",
|
||||
"Defender:DID_DefenderShotgun_Basic_R_T01",
|
||||
"Schematic:SID_Pistol_Handcannon_SR_Ore_T01",
|
||||
"Defender:DID_DefenderShotgun_Basic_C_T01",
|
||||
"Schematic:SID_Pistol_Handcannon_R_Ore_T01",
|
||||
"Defender:DID_DefenderPistol_Founders_VR_T01",
|
||||
"Schematic:SID_Pistol_Handcannon_Founders_VR_Ore_T01",
|
||||
"Defender:DID_DefenderPistol_Basic_VR_T01",
|
||||
"Schematic:SID_Pistol_Gatling_VR_Ore_T01",
|
||||
"Defender:DID_DefenderPistol_Basic_UC_T01",
|
||||
"Schematic:SID_Pistol_Gatling_SR_Ore_T01",
|
||||
"Defender:DID_DefenderPistol_Basic_SR_T01",
|
||||
"Schematic:SID_Pistol_FireCracker_VR_Ore_T01",
|
||||
"Defender:DID_DefenderPistol_Basic_R_T01",
|
||||
"Schematic:SID_Pistol_FireCracker_SR_Ore_T01",
|
||||
"Defender:DID_DefenderPistol_Basic_C_T01",
|
||||
"Schematic:SID_Pistol_FireCracker_R_Ore_T01",
|
||||
"Defender:DID_DefenderMelee_Basic_VR_T01",
|
||||
"Schematic:SID_Pistol_Dragon_VR_Ore_T01",
|
||||
"Defender:DID_DefenderMelee_Basic_UC_T01",
|
||||
"Schematic:SID_Pistol_Dragon_SR_Ore_T01",
|
||||
"Defender:DID_DefenderMelee_Basic_SR_T01",
|
||||
"Schematic:SID_Pistol_BoltRevolver_UC_Ore_T01",
|
||||
"Defender:DID_DefenderMelee_Basic_R_T01",
|
||||
"Schematic:SID_Pistol_BoltRevolver_R_Ore_T01",
|
||||
"Defender:DID_DefenderMelee_Basic_C_T01",
|
||||
"Schematic:SID_Pistol_BoltRevolver_C_Ore_T01",
|
||||
"Defender:DID_DefenderAssault_Founders_VR_T01",
|
||||
"Schematic:SID_Pistol_Bolt_VR_Ore_T01",
|
||||
"Defender:DID_DefenderAssault_Basic_VR_T01",
|
||||
"Schematic:SID_Pistol_Bolt_SR_Ore_T01",
|
||||
"Defender:DID_DefenderAssault_Basic_UC_T01",
|
||||
"Schematic:SID_Pistol_AutoHeavy_VR_Ore_T01",
|
||||
"Defender:DID_DefenderAssault_Basic_SR_T01",
|
||||
"Schematic:SID_Pistol_AutoHeavy_SR_Ore_T01",
|
||||
"Defender:DID_DefenderAssault_Basic_R_T01",
|
||||
"Schematic:SID_Pistol_AutoHeavy_Founders_VR_Ore_T01",
|
||||
"Defender:DID_DefenderAssault_Basic_C_T01",
|
||||
"Schematic:SID_Pistol_AutoHeavy_Founders_SR_Ore_T01",
|
||||
"Schematic:SID_Pistol_AutoHeavy_R_Ore_T01",
|
||||
"Schematic:SID_Pistol_AutoHeavy_Founders_R_Ore_T01",
|
||||
"Schematic:SID_Pistol_Auto_VR_Ore_T01",
|
||||
"Schematic:SID_Pistol_Auto_SR_Ore_T01",
|
||||
"Schematic:SID_Pistol_Auto_UC_Ore_T01",
|
||||
"Schematic:SID_Pistol_Auto_R_Ore_T01",
|
||||
"Schematic:SID_Pistol_Auto_C_Ore_T01",
|
||||
"Schematic:SID_Launcher_Rocket_VR_Ore_T01",
|
||||
"Schematic:SID_Launcher_Rocket_SR_Ore_T01",
|
||||
"Schematic:SID_Launcher_Rocket_R_Ore_T01",
|
||||
"Schematic:SID_Launcher_Pumpkin_RPG_SR_Ore_T01",
|
||||
"Schematic:SID_Launcher_Hydraulic_VR_Ore_T01",
|
||||
"Schematic:SID_Launcher_Hydraulic_SR_Ore_T01",
|
||||
"Schematic:SID_Launcher_Grenade_VR_Ore_T01",
|
||||
"Schematic:SID_Launcher_Grenade_SR_Ore_T01",
|
||||
"Schematic:SID_Launcher_Grenade_R_Ore_T01",
|
||||
"Schematic:SID_Assault_Surgical_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_Surgical_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_SingleShot_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_SingleShot_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_SingleShot_R_Ore_T01",
|
||||
"Schematic:SID_Assault_SemiAuto_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_SemiAuto_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_SemiAuto_Founders_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_SemiAuto_UC_Ore_T01",
|
||||
"Schematic:SID_Assault_SemiAuto_R_Ore_T01",
|
||||
"Schematic:SID_Assault_SemiAuto_C_Ore_T01",
|
||||
"Schematic:SID_Assault_Raygun_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_Raygun_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Surgical_Drum_Founders_R_Ore_T01",
|
||||
"Schematic:SID_Assault_LMG_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_LMG_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_LMG_R_Ore_T01",
|
||||
"Schematic:SID_Assault_LMG_Drum_Founders_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_LMG_Drum_Founders_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Hydra_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Doubleshot_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_Doubleshot_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Burst_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_Burst_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Burst_UC_Ore_T01",
|
||||
"Schematic:SID_Assault_Burst_R_Ore_T01",
|
||||
"Schematic:SID_Assault_Burst_C_Ore_T01",
|
||||
"Schematic:SID_Assault_Auto_VR_Ore_T01",
|
||||
"Schematic:SID_Assault_Auto_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Auto_Halloween_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Auto_Founders_SR_Ore_T01",
|
||||
"Schematic:SID_Assault_Auto_UC_Ore_T01",
|
||||
"Schematic:SID_Assault_Auto_R_Ore_T01",
|
||||
"Schematic:SID_Assault_Auto_C_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_Military_VR_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_Military_SR_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_Military_R_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_C_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_Laser_VR_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_Laser_SR_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_VR_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_SR_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_UC_Ore_T01",
|
||||
"Schematic:SID_Piercing_Spear_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_C_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_Laser_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_Laser_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_Laser_Founders_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_Laser_Founders_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_Laser_Founders_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_UC_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Medium_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Light_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Light_UC_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Light_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Light_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Light_Founders_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Light_C_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Hydraulic_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Hydraulic_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Heavy_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Heavy_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Heavy_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Heavy_Founders_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Heavy_UC_Ore_T01",
|
||||
"Schematic:SID_Edged_Sword_Heavy_C_Ore_T01",
|
||||
"Schematic:SID_Edged_Scythe_C_Ore_T01",
|
||||
"Schematic:SID_Edged_Scythe_Laser_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Scythe_Laser_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Scythe_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Scythe_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Scythe_UC_Ore_T01",
|
||||
"Schematic:SID_Edged_Scythe_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_C_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_Laser_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_Laser_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_Founders_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_UC_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Medium_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Light_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Light_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Light_UC_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Light_R_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Light_C_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Heavy_C_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Heavy_VR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Heavy_SR_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Heavy_UC_Ore_T01",
|
||||
"Schematic:SID_Edged_Axe_Heavy_R_Ore_T01",
|
||||
"Schematic:SID_Blunt_Medium_C_Ore_T01",
|
||||
"Schematic:SID_Blunt_Medium_VR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Medium_SR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Medium_UC_Ore_T01",
|
||||
"Schematic:SID_Blunt_Medium_R_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_VR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_SR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Tool_Light_UC_Ore_T01",
|
||||
"Schematic:SID_Blunt_Tool_Light_R_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Rocket_VR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Rocket_SR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Heavy_C_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Heavy_VR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Heavy_SR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Heavy_Founders_VR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Heavy_UC_Ore_T01",
|
||||
"Schematic:SID_Blunt_Hammer_Heavy_R_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_Rocketbat_VR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_Rocketbat_SR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_C_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_Bat_UC_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_Bat_R_Ore_T01",
|
||||
"Schematic:SID_Blunt_Club_Light_VR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Club_Light_SR_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_UC_Ore_T01",
|
||||
"Schematic:SID_Blunt_Light_R_Ore_T01",
|
||||
"Schematic:SID_Blunt_Heavy_Paddle_UC_Ore_T01",
|
||||
"Schematic:SID_Blunt_Heavy_Paddle_R_Ore_T01",
|
||||
"Schematic:SID_Blunt_Heavy_Paddle_C_Ore_T01"
|
||||
]
|
||||
7
assets/responses/SAC.json
Normal file
7
assets/responses/SAC.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"lawin",
|
||||
"ti93",
|
||||
"pro100katyt",
|
||||
"playeereq",
|
||||
"matteoki"
|
||||
]
|
||||
56
assets/responses/SeasonData.json
Normal file
56
assets/responses/SeasonData.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"Season2": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season3": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season4": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season5": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season6": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season7": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season8": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season9": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
},
|
||||
"Season10": {
|
||||
"battlePassPurchased": false,
|
||||
"battlePassTier": 1,
|
||||
"battlePassXPBoost": 0,
|
||||
"battlePassXPFriendBoost": 0
|
||||
}
|
||||
}
|
||||
3403
assets/responses/catalog.json
Normal file
3403
assets/responses/catalog.json
Normal file
File diff suppressed because it is too large
Load Diff
6018
assets/responses/contentpages.json
Normal file
6018
assets/responses/contentpages.json
Normal file
File diff suppressed because it is too large
Load Diff
1351
assets/responses/dailyrewards.json
Normal file
1351
assets/responses/dailyrewards.json
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/responses/friendslist.json
Normal file
1
assets/responses/friendslist.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
10
assets/responses/friendslist2.json
Normal file
10
assets/responses/friendslist2.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"friends": [],
|
||||
"incoming": [],
|
||||
"outgoing": [],
|
||||
"suggested": [],
|
||||
"blocklist": [],
|
||||
"settings": {
|
||||
"acceptInvites": "public"
|
||||
}
|
||||
}
|
||||
1075
assets/responses/keychain.json
Normal file
1075
assets/responses/keychain.json
Normal file
File diff suppressed because it is too large
Load Diff
67
assets/responses/motdTarget.json
Normal file
67
assets/responses/motdTarget.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"contentType": "collection",
|
||||
"contentId": "motd-default-collection",
|
||||
"tcId": "634e8e85-e2fc-4c68-bb10-93604cf6605f",
|
||||
"contentItems": [
|
||||
{
|
||||
"contentType": "content-item",
|
||||
"contentId": "46874c56-0973-4cbe-ac98-b580c5b36df5",
|
||||
"tcId": "61fb3dd8-f23d-45cc-9058-058ab223ba5c",
|
||||
"contentFields": {
|
||||
"body": {
|
||||
"ar": "استمتع بتجربة لعب استثنائية!",
|
||||
"en": "Have a phenomenal gaming experience!",
|
||||
"de": "Wünsche allen ein wunderbares Spielerlebnis!",
|
||||
"es": "¡Que disfrutes de tu experiencia de videojuegos!",
|
||||
"es-419": "¡Ten una experiencia de juego espectacular!",
|
||||
"fr": "Un bon jeu à toutes et à tous !",
|
||||
"it": "Ti auguriamo un'esperienza di gioco fenomenale!",
|
||||
"ja": "驚きの体験をしよう!",
|
||||
"ko": "게임에서 환상적인 경험을 해보세요!",
|
||||
"pl": "Życzymy fenomenalnej gry!",
|
||||
"pt-BR": "Tenha uma experiência de jogo fenomenal!",
|
||||
"ru": "Желаю невероятно приятной игры!",
|
||||
"tr": "Muhteşem bir oyun deneyimi yaşamanı dileriz!"
|
||||
},
|
||||
"entryType": "Website",
|
||||
"image": [
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"url": "https://fortnite-public-service-prod11.ol.epicgames.com/images/motd.png"
|
||||
}
|
||||
],
|
||||
"tabTitleOverride": "LawinServer",
|
||||
"tileImage": [
|
||||
{
|
||||
"width": 1024,
|
||||
"height": 512,
|
||||
"url": "https://fortnite-public-service-prod11.ol.epicgames.com/images/motd-s.png"
|
||||
}
|
||||
],
|
||||
"title": {
|
||||
"ar": "مرحبًا بك في LawinServer!",
|
||||
"en": "Welcome to LawinServer!",
|
||||
"de": "Willkommen bei LawinServer!",
|
||||
"es": "¡Bienvenidos a LawinServer!",
|
||||
"es-419": "¡Bienvenidos a LawinServer!",
|
||||
"fr": "Bienvenue sur LawinServer !",
|
||||
"it": "Benvenuto in LawinServer!",
|
||||
"ja": "LawinServerへようこそ!",
|
||||
"ko": "LawinServer에 오신 것을 환영합니다!",
|
||||
"pl": "Witaj w LawinServerze!",
|
||||
"pt-BR": "Bem-vindo ao LawinServer!",
|
||||
"ru": "Добро пожаловать в LawinServer!",
|
||||
"tr": "LavinServer'a Hoş Geldiniz!"
|
||||
},
|
||||
"videoAutoplay": false,
|
||||
"videoLoop": false,
|
||||
"videoMute": false,
|
||||
"videoStreamingEnabled": false,
|
||||
"websiteButtonText": "Discord",
|
||||
"websiteURL": "https://discord.gg/KJ8UaHZ"
|
||||
},
|
||||
"contentSchemaName": "MotdWebsiteNewsWithVideo"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
assets/responses/privacy.json
Normal file
4
assets/responses/privacy.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"accountId": "",
|
||||
"optOutOfPublicLeaderboards": false
|
||||
}
|
||||
121549
assets/responses/quests.json
Normal file
121549
assets/responses/quests.json
Normal file
File diff suppressed because it is too large
Load Diff
2147
assets/responses/transformItemIDS.json
Normal file
2147
assets/responses/transformItemIDS.json
Normal file
File diff suppressed because it is too large
Load Diff
36
assets/responses/winterfestrewards.json
Normal file
36
assets/responses/winterfestrewards.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"author": "List made by PRO100KatYT",
|
||||
"Season11": {
|
||||
"ERG.Node.A.1": "AthenaCharacter:cid_645_athena_commando_f_wolly",
|
||||
"ERG.Node.B.1": "AthenaGlider:glider_id_188_galileorocket_g7oki",
|
||||
"ERG.Node.C.1": "AthenaBackpack:bid_430_galileospeedboat_9rxe3",
|
||||
"ERG.Node.D.1": "AthenaCharacter:cid_643_athena_commando_m_ornamentsoldier",
|
||||
"ERG.Node.A.2": "AthenaPickaxe:pickaxe_id_329_gingerbreadcookie1h",
|
||||
"ERG.Node.A.3": "AthenaPickaxe:pickaxe_id_332_mintminer",
|
||||
"ERG.Node.A.4": "AthenaDance:eid_snowglobe",
|
||||
"ERG.Node.A.5": "AthenaGlider:glider_id_191_pinetree",
|
||||
"ERG.Node.A.6": "AthenaItemWrap:wrap_188_wrappingpaper",
|
||||
"ERG.Node.A.7": "AthenaItemWrap:wrap_183_newyear2020",
|
||||
"ERG.Node.A.8": "AthenaSkyDiveContrail:trails_id_082_holidaygarland",
|
||||
"ERG.Node.A.9": "AthenaMusicPack:musicpack_040_xmaschiptunes",
|
||||
"ERG.Node.A.10": "AthenaLoadingScreen:lsid_208_smpattern",
|
||||
"ERG.Node.A.11": "AthenaLoadingScreen:lsid_209_akcrackshot"
|
||||
},
|
||||
"Season19": {
|
||||
"ERG.Node.A.1": "Token:14daysoffortnite_small_giftbox",
|
||||
"ERG.Node.B.1": "AthenaDance:emoji_s19_animwinterfest2021",
|
||||
"ERG.Node.C.1": "AthenaGlider:glider_id_335_logarithm_40qgl",
|
||||
"ERG.Node.D.1": "AthenaCharacter:cid_a_323_athena_commando_m_bananawinter",
|
||||
"ERG.Node.A.2": "HomebaseBannerIcon:brs19_winterfest2021",
|
||||
"ERG.Node.A.3": "AthenaSkyDiveContrail:trails_id_137_turtleneckcrystal",
|
||||
"ERG.Node.A.4": "AthenaItemWrap:wrap_429_holidaysweater",
|
||||
"ERG.Node.A.5": "AthenaLoadingScreen:lsid_393_winterfest2021",
|
||||
"ERG.Node.A.6": "AthenaMusicPack:musicpack_117_winterfest2021",
|
||||
"ERG.Node.A.7": "AthenaDance:eid_epicyarn",
|
||||
"ERG.Node.A.8": "AthenaCharacter:cid_a_310_athena_commando_F_scholarfestive",
|
||||
"ERG.Node.A.9": "AthenaPickaxe:pickaxe_id_731_scholarfestivefemale1h",
|
||||
"ERG.Node.A.10": "AthenaItemWrap:wrap_430_winterlights",
|
||||
"ERG.Node.A.11": "AthenaDance:spid_346_winterfest_2021",
|
||||
"ERG.Node.A.12": "AthenaPickaxe:pickaxe_ID_732_shovelmale"
|
||||
}
|
||||
}
|
||||
41781
assets/responses/worldstw.json
Normal file
41781
assets/responses/worldstw.json
Normal file
File diff suppressed because it is too large
Load Diff
50
lib/cli.dart
50
lib/cli.dart
@@ -12,6 +12,12 @@ import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
||||
|
||||
late String? username;
|
||||
late GameType type;
|
||||
late bool verbose;
|
||||
late String dll;
|
||||
late FortniteVersion version;
|
||||
late bool autoRestart;
|
||||
|
||||
void main(List<String> args){
|
||||
handleCLI(args);
|
||||
@@ -20,7 +26,7 @@ void main(List<String> args){
|
||||
Future<void> handleCLI(List<String> args) async {
|
||||
stdout.writeln("Reboot Launcher");
|
||||
stdout.writeln("Wrote by Auties00");
|
||||
stdout.writeln("Version 4.4");
|
||||
stdout.writeln("Version 5.3");
|
||||
|
||||
kill();
|
||||
|
||||
@@ -31,16 +37,17 @@ Future<void> handleCLI(List<String> args) async {
|
||||
var parser = ArgParser()
|
||||
..addCommand("list")
|
||||
..addCommand("launch")
|
||||
..addOption("version", defaultsTo: gameJson["version"])
|
||||
..addOption("version")
|
||||
..addOption("username")
|
||||
..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson))
|
||||
..addOption("server-host")
|
||||
..addOption("server-port")
|
||||
..addOption("matchmaking-address")
|
||||
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path)
|
||||
..addOption("type", allowed: getGameTypes(), defaultsTo: getDefaultGameType(gameJson))
|
||||
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
|
||||
..addFlag("log", defaultsTo: false)
|
||||
..addFlag("memory-fix", defaultsTo: false, negatable: true);
|
||||
..addFlag("auto-restart", defaultsTo: false, negatable: true);
|
||||
var result = parser.parse(args);
|
||||
if (result.command?.name == "list") {
|
||||
stdout.writeln("Versions list: ");
|
||||
@@ -49,45 +56,46 @@ Future<void> handleCLI(List<String> args) async {
|
||||
return;
|
||||
}
|
||||
|
||||
var dll = result["dll"];
|
||||
var type = getGameType(result);
|
||||
var username = result["username"];
|
||||
username ??= gameJson["${type == GameType.client ? "game" : "server"}_username"];
|
||||
var verbose = result["log"];
|
||||
dll = result["dll"];
|
||||
type = getGameType(result);
|
||||
username = result["username"] ?? gameJson["${type == GameType.client ? "game" : "server"}_username"];
|
||||
verbose = result["log"];
|
||||
|
||||
var dummyVersion = _createVersion(gameJson["version"], result["version"], result["memory-fix"], versions);
|
||||
version = _createVersion(gameJson["version"], result["version"], versions);
|
||||
await downloadRequiredDLLs();
|
||||
if(result["update"]) {
|
||||
stdout.writeln("Updating reboot dll...");
|
||||
await downloadRebootDll(0);
|
||||
try {
|
||||
await downloadRebootDll(0);
|
||||
}catch(error){
|
||||
stderr.writeln("Cannot update reboot dll: $error");
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln("Launching game(type: ${type.name})...");
|
||||
if(dummyVersion.executable == null){
|
||||
throw Exception("Missing game executable at: ${dummyVersion.location.path}");
|
||||
if(version.executable == null){
|
||||
throw Exception("Missing game executable at: ${version.location.path}");
|
||||
}
|
||||
|
||||
if (result["type"] == "headless_server") {
|
||||
await patchHeadless(dummyVersion.executable!);
|
||||
}else if(result["type"] == "client"){
|
||||
await patchMatchmaking(dummyVersion.executable!);
|
||||
}
|
||||
await patchHeadless(version.executable!);
|
||||
await patchMatchmaking(version.executable!);
|
||||
|
||||
var serverType = getServerType(result);
|
||||
var host = result["server-host"] ?? serverJson["${serverType.id}_host"];
|
||||
var port = result["server-port"] ?? serverJson["${serverType.id}_port"];
|
||||
var started = await startServer(host, port, serverType);
|
||||
var started = await startServer(host, port, serverType, result["matchmaking-address"]);
|
||||
if(!started){
|
||||
stderr.writeln("Cannot start server!");
|
||||
return;
|
||||
}
|
||||
|
||||
await startGame(username, type, verbose, dll, dummyVersion);
|
||||
autoRestart = result["auto-restart"];
|
||||
await startGame();
|
||||
}
|
||||
|
||||
FortniteVersion _createVersion(String? versionName, String? versionPath, bool memoryFix, List<FortniteVersion> versions) {
|
||||
FortniteVersion _createVersion(String? versionName, String? versionPath, List<FortniteVersion> versions) {
|
||||
if (versionPath != null) {
|
||||
return FortniteVersion(name: "dummy", location: Directory(versionPath), memoryFix: memoryFix);
|
||||
return FortniteVersion(name: "dummy", location: Directory(versionPath));
|
||||
}
|
||||
|
||||
if(versionName != null){
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
|
||||
show WinDesktopWindow;
|
||||
import 'package:dart_vlc/dart_vlc.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
@@ -16,6 +17,7 @@ import 'package:reboot_launcher/src/page/home_page.dart';
|
||||
import 'package:reboot_launcher/src/util/error.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
|
||||
@@ -28,6 +30,7 @@ void main(List<String> args) async {
|
||||
}
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartVLC.initialize();
|
||||
|
||||
await SystemTheme.accentColor.load();
|
||||
await GetStorage.init("game");
|
||||
@@ -39,11 +42,18 @@ void main(List<String> args) async {
|
||||
Get.put(BuildController());
|
||||
Get.put(SettingsController());
|
||||
doWhenWindowReady(() {
|
||||
const size = Size(600, 365);
|
||||
var controller = Get.find<SettingsController>();
|
||||
var size = Size(controller.width, controller.height);
|
||||
var window = appWindow as WinDesktopWindow;
|
||||
window.setWindowCutOnMaximize(appBarSize * 2);
|
||||
appWindow.size = size;
|
||||
appWindow.alignment = Alignment.center;
|
||||
if(controller.offsetX != null && controller.offsetY != null){
|
||||
appWindow.position = Offset(controller.offsetX!, controller.offsetY!);
|
||||
}else {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
|
||||
windowManager.setPreventClose(true);
|
||||
appWindow.title = "Reboot Launcher";
|
||||
appWindow.show();
|
||||
});
|
||||
|
||||
49
lib/src/cli/compatibility.dart
Normal file
49
lib/src/cli/compatibility.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
Future<Map<String, dynamic>> getControllerJson(String name) async {
|
||||
var folder = await _getWindowsPath(FOLDERID_Documents);
|
||||
if(folder == null){
|
||||
throw Exception("Missing documents folder");
|
||||
}
|
||||
|
||||
var file = File("$folder/$name.gs");
|
||||
if(!file.existsSync()){
|
||||
return HashMap();
|
||||
}
|
||||
|
||||
return jsonDecode(file.readAsStringSync());
|
||||
}
|
||||
|
||||
Future<String?> _getWindowsPath(String folderID) {
|
||||
final Pointer<Pointer<Utf16>> pathPtrPtr = calloc<Pointer<Utf16>>();
|
||||
final Pointer<GUID> knownFolderID = calloc<GUID>()..ref.setGUID(folderID);
|
||||
|
||||
try {
|
||||
final int hr = SHGetKnownFolderPath(
|
||||
knownFolderID,
|
||||
KF_FLAG_DEFAULT,
|
||||
NULL,
|
||||
pathPtrPtr,
|
||||
);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
if (hr == E_INVALIDARG || hr == E_FAIL) {
|
||||
throw WindowsException(hr);
|
||||
}
|
||||
return Future<String?>.value();
|
||||
}
|
||||
|
||||
final String path = pathPtrPtr.value.toDartString();
|
||||
return Future<String>.value(path);
|
||||
} finally {
|
||||
calloc.free(pathPtrPtr);
|
||||
calloc.free(knownFolderID);
|
||||
}
|
||||
}
|
||||
52
lib/src/cli/config.dart
Normal file
52
lib/src/cli/config.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
|
||||
import '../model/fortnite_version.dart';
|
||||
import '../model/game_type.dart';
|
||||
import '../model/server_type.dart';
|
||||
|
||||
Iterable<String> getGameTypes() => GameType.values.map((entry) => entry.id);
|
||||
|
||||
Iterable<String> getServerTypes() => ServerType.values.map((entry) => entry.id);
|
||||
|
||||
String getDefaultServerType(Map<String, dynamic> json) {
|
||||
var type = ServerType.values.elementAt(json["type"] ?? 0);
|
||||
return type.id;
|
||||
}
|
||||
|
||||
GameType getGameType(ArgResults result) {
|
||||
var type = GameType.of(result["type"]);
|
||||
if(type == null){
|
||||
throw Exception("Unknown game type: $result. Use --type only with ${getGameTypes().join(", ")}");
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
ServerType getServerType(ArgResults result) {
|
||||
var type = ServerType.of(result["server-type"]);
|
||||
if(type == null){
|
||||
throw Exception("Unknown server type: $result. Use --server-type only with ${getServerTypes().join(", ")}");
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
String getDefaultGameType(Map<String, dynamic> json){
|
||||
var type = GameType.values.elementAt(json["type"] ?? 0);
|
||||
switch(type){
|
||||
case GameType.client:
|
||||
return "client";
|
||||
case GameType.server:
|
||||
return "server";
|
||||
case GameType.headlessServer:
|
||||
return "headless_server";
|
||||
}
|
||||
}
|
||||
|
||||
List<FortniteVersion> getVersions(Map<String, dynamic> gameJson) {
|
||||
Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]");
|
||||
return iterable.map((entry) => FortniteVersion.fromJson(entry))
|
||||
.toList();
|
||||
}
|
||||
137
lib/src/cli/game.dart
Normal file
137
lib/src/cli/game.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/cli.dart';
|
||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
||||
|
||||
import '../model/fortnite_version.dart';
|
||||
import '../model/game_type.dart';
|
||||
import '../util/injector.dart';
|
||||
import '../util/os.dart';
|
||||
import '../util/server.dart';
|
||||
|
||||
final List<String> _errorStrings = [
|
||||
"port 3551 failed: Connection refused",
|
||||
"Unable to login to Fortnite servers",
|
||||
"HTTP 400 response from ",
|
||||
"Network failure when attempting to check platform restrictions",
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
|
||||
Process? _gameProcess;
|
||||
Process? _launcherProcess;
|
||||
Process? _eacProcess;
|
||||
|
||||
Future<void> startGame() async {
|
||||
await _startLauncherProcess(version);
|
||||
await _startEacProcess(version);
|
||||
|
||||
var gamePath = version.executable?.path;
|
||||
if (gamePath == null) {
|
||||
throw Exception("${version.location
|
||||
.path} no longer contains a Fortnite executable, did you delete or move it?");
|
||||
}
|
||||
|
||||
var hosting = type != GameType.client;
|
||||
if (username == null) {
|
||||
username = "Reboot${hosting ? 'Host' : 'Player'}";
|
||||
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
|
||||
}
|
||||
|
||||
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type))
|
||||
..exitCode.then((_) => _onClose())
|
||||
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
|
||||
_injectOrShowError("craniumv2.dll");
|
||||
}
|
||||
|
||||
|
||||
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
||||
if (dummyVersion.launcher == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
|
||||
Win32Process(_launcherProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
||||
if (dummyVersion.eacExecutable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
||||
Win32Process(_eacProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
||||
if(verbose) {
|
||||
stdout.writeln(line);
|
||||
}
|
||||
|
||||
if(line.contains("Platform has ")){
|
||||
_injectOrShowError("craniumv2.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) {
|
||||
_onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if(_errorStrings.any((element) => line.contains(element))){
|
||||
stderr.writeln("The backend doesn't work! Token expired");
|
||||
_onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Region ")){
|
||||
_injectRequiredDLLs(hosting, dll);
|
||||
}
|
||||
}
|
||||
|
||||
void _injectRequiredDLLs(bool host, String rebootDll) {
|
||||
if(host) {
|
||||
_injectOrShowError(rebootDll, false);
|
||||
}else {
|
||||
_injectOrShowError("console.dll");
|
||||
}
|
||||
|
||||
_injectOrShowError("leakv2.dll");
|
||||
}
|
||||
|
||||
void _kill() {
|
||||
_gameProcess?.kill(ProcessSignal.sigabrt);
|
||||
_launcherProcess?.kill(ProcessSignal.sigabrt);
|
||||
_eacProcess?.kill(ProcessSignal.sigabrt);
|
||||
}
|
||||
|
||||
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
|
||||
if (_gameProcess == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
stdout.writeln("Injecting $binary...");
|
||||
var dll = locate ? await loadBinary(binary, true) : File(binary);
|
||||
if(!dll.existsSync()){
|
||||
throw Exception("Cannot inject $dll: missing file");
|
||||
}
|
||||
|
||||
await injectDll(_gameProcess!.pid, dll.path);
|
||||
} catch (exception) {
|
||||
throw Exception("Cannot inject binary: $binary");
|
||||
}
|
||||
}
|
||||
|
||||
void _onClose() {
|
||||
_kill();
|
||||
sleep(const Duration(seconds: 3));
|
||||
stdout.writeln("The game was closed");
|
||||
if(autoRestart){
|
||||
stdout.writeln("Restarting automatically game");
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
59
lib/src/cli/reboot.dart
Normal file
59
lib/src/cli/reboot.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
const String _baseDownload = "https://cdn.discordapp.com/attachments/1009257632315494520/1051137082766131250/Cranium.dll";
|
||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll";
|
||||
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1013220721494863872/1033484506633617500/MemoryLeakFixer.dll";
|
||||
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
|
||||
|
||||
Future<void> downloadRequiredDLLs() async {
|
||||
stdout.writeln("Downloading necessary components...");
|
||||
var consoleDll = await loadBinary("console.dll", true);
|
||||
if(!consoleDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_consoleDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download console.dll");
|
||||
}
|
||||
|
||||
await consoleDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var craniumDll = await loadBinary("craniumv2.dll", true);
|
||||
if(!craniumDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_baseDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download craniumv2.dll");
|
||||
}
|
||||
|
||||
await craniumDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var memoryFixDll = await loadBinary("leakv2.dll", true);
|
||||
if(!memoryFixDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_memoryFixDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download leakv2.dll");
|
||||
}
|
||||
|
||||
await memoryFixDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var config = loadEmbedded("config/");
|
||||
var profiles = loadEmbedded("profiles/");
|
||||
var responses = loadEmbedded("responses/");
|
||||
if(!config.existsSync() || !profiles.existsSync() || !responses.existsSync()){
|
||||
var response = await http.get(Uri.parse(_embeddedConfigDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download embedded server config");
|
||||
}
|
||||
|
||||
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
|
||||
await extractFileToDisk(tempZip.path, "$safeBinariesDirectory\\backend\\cli");
|
||||
}
|
||||
}
|
||||
88
lib/src/cli/server.dart
Normal file
88
lib/src/cli/server.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/embedded/server.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
|
||||
import '../model/server_type.dart';
|
||||
import '../util/server.dart';
|
||||
import 'game.dart';
|
||||
|
||||
Future<bool> startServer(String? host, String? port, ServerType type, String? matchmakingIp) async {
|
||||
stdout.writeln("Starting backend server...");
|
||||
switch(type){
|
||||
case ServerType.local:
|
||||
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
|
||||
if(result == null){
|
||||
throw Exception("Local backend server is not running");
|
||||
}
|
||||
|
||||
stdout.writeln("Detected local backend server");
|
||||
return true;
|
||||
case ServerType.embedded:
|
||||
stdout.writeln("Starting an embedded server...");
|
||||
await startEmbeddedServer(
|
||||
() => matchmakingIp ?? "127.0.0.1"
|
||||
);
|
||||
await startEmbeddedMatchmaker();
|
||||
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
|
||||
if(result == null){
|
||||
throw Exception("Cannot start embedded server");
|
||||
}
|
||||
|
||||
return true;
|
||||
case ServerType.remote:
|
||||
if(host == null){
|
||||
throw Exception("Missing host for remote server");
|
||||
}
|
||||
|
||||
if(port == null){
|
||||
throw Exception("Missing host for remote server");
|
||||
}
|
||||
|
||||
stdout.writeln("Starting a reverse proxy to $host:$port");
|
||||
return await _changeReverseProxyState(host, port) != null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<HttpServer?> _changeReverseProxyState(String host, String port) async {
|
||||
host = host.trim();
|
||||
if(host.isEmpty){
|
||||
throw Exception("Missing host name");
|
||||
}
|
||||
|
||||
port = port.trim();
|
||||
if(port.isEmpty){
|
||||
throw Exception("Missing port");
|
||||
}
|
||||
|
||||
if(int.tryParse(port) == null){
|
||||
throw Exception("Invalid port, use only numbers");
|
||||
}
|
||||
|
||||
try{
|
||||
var uri = await ping(host, port);
|
||||
if(uri == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
}catch(error){
|
||||
throw Exception("Cannot start reverse proxy");
|
||||
}
|
||||
}
|
||||
|
||||
void kill() async {
|
||||
var shell = Shell(
|
||||
commandVerbose: false,
|
||||
commentVerbose: false,
|
||||
verbose: false
|
||||
);
|
||||
try {
|
||||
await shell.run("taskkill /f /im FortniteLauncher.exe");
|
||||
await shell.run("taskkill /f /im FortniteClient-Win64-Shipping_EAC.exe");
|
||||
}catch(_){
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,23 @@ import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
class BuildController extends GetxController {
|
||||
List<FortniteBuild>? builds;
|
||||
FortniteBuild? _selectedBuild;
|
||||
final List<Function()> _listeners;
|
||||
late RxBool cancelledDownload;
|
||||
|
||||
BuildController() {
|
||||
BuildController() : _listeners = [] {
|
||||
cancelledDownload = RxBool(false);
|
||||
}
|
||||
|
||||
FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0);
|
||||
|
||||
set selectedBuild(FortniteBuild build) => _selectedBuild = build;
|
||||
set selectedBuild(FortniteBuild build) {
|
||||
_selectedBuild = build;
|
||||
for (var listener in _listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
void addOnBuildChangedListener(Function() listener) => _listeners.add(listener);
|
||||
|
||||
void removeOnBuildChangedListener() => _listeners.clear();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
@@ -90,7 +91,7 @@ class GameController extends GetxController {
|
||||
_storage.write("version", version?.name);
|
||||
}
|
||||
|
||||
void rename(FortniteVersion version, String result) {
|
||||
versions.update((val) => version.name = result);
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||
versions.update((val) => function(version));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ import 'dart:io';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
import '../dialog/snackbar.dart';
|
||||
import '../model/server_type.dart';
|
||||
|
||||
class ServerController extends GetxController {
|
||||
@@ -19,9 +17,9 @@ class ServerController extends GetxController {
|
||||
late final Rx<ServerType> type;
|
||||
late final RxBool warning;
|
||||
late RxBool started;
|
||||
late int embeddedServerCounter;
|
||||
Process? embeddedServer;
|
||||
HttpServer? reverseProxy;
|
||||
Jaguar? embeddedServer;
|
||||
Jaguar? embeddedMatchmaker;
|
||||
HttpServer? remoteServer;
|
||||
|
||||
ServerController() {
|
||||
_storage = GetStorage("server");
|
||||
@@ -37,8 +35,8 @@ class ServerController extends GetxController {
|
||||
}
|
||||
|
||||
if(value == ServerType.remote){
|
||||
reverseProxy?.close(force: true);
|
||||
reverseProxy = null;
|
||||
remoteServer?.close(force: true);
|
||||
remoteServer = null;
|
||||
started.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -56,8 +54,6 @@ class ServerController extends GetxController {
|
||||
warning.listen((value) => _storage.write("lawin_value", value));
|
||||
|
||||
started = RxBool(false);
|
||||
|
||||
embeddedServerCounter = 0;
|
||||
}
|
||||
|
||||
String _readHost() {
|
||||
@@ -70,83 +66,16 @@ class ServerController extends GetxController {
|
||||
return _storage.read("${type.value.id}_port") ?? _serverPort;
|
||||
}
|
||||
|
||||
Future<ServerResult> start(bool needsFreePort) async {
|
||||
var lastCounter = ++embeddedServerCounter;
|
||||
var result = await checkServerPreconditions(host.text, port.text, type.value, needsFreePort);
|
||||
if(result.type != ServerResultType.canStart){
|
||||
return result;
|
||||
}
|
||||
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
await _startEmbeddedServer();
|
||||
embeddedServer?.exitCode.then((value) async {
|
||||
if (!started() || lastCounter != embeddedServerCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
started.value = false;
|
||||
await freeLawinPort();
|
||||
showUnexpectedError();
|
||||
});
|
||||
break;
|
||||
case ServerType.remote:
|
||||
var uriResult = await result.uri!;
|
||||
if(uriResult == null){
|
||||
return ServerResult(
|
||||
type: ServerResultType.cannotPingServer
|
||||
);
|
||||
}
|
||||
|
||||
reverseProxy = await startRemoteServer(uriResult);
|
||||
break;
|
||||
case ServerType.local:
|
||||
break;
|
||||
}
|
||||
}catch(error, stackTrace){
|
||||
return ServerResult(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
type: ServerResultType.unknownError
|
||||
);
|
||||
}
|
||||
|
||||
var myself = await pingSelf(port.text);
|
||||
if(myself == null){
|
||||
return ServerResult(
|
||||
type: ServerResultType.cannotPingServer,
|
||||
pid: embeddedServer?.pid
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
type: ServerResultType.started
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startEmbeddedServer() async {
|
||||
var result = await startEmbeddedServer();
|
||||
if(result != null){
|
||||
embeddedServer = result;
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage("The server is corrupted, trying to fix it");
|
||||
await serverLocation.parent.delete(recursive: true);
|
||||
await downloadServerInteractive(true);
|
||||
await _startEmbeddedServer();
|
||||
}
|
||||
|
||||
Future<bool> stop() async {
|
||||
started.value = false;
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
await freeLawinPort();
|
||||
await embeddedServer?.close();
|
||||
await embeddedMatchmaker?.close();
|
||||
break;
|
||||
case ServerType.remote:
|
||||
await reverseProxy?.close(force: true);
|
||||
await remoteServer?.close(force: true);
|
||||
break;
|
||||
case ServerType.local:
|
||||
break;
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
|
||||
import 'package:dart_vlc/dart_vlc.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:ini/ini.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController rebootDll;
|
||||
late final TextEditingController consoleDll;
|
||||
late final TextEditingController craniumDll;
|
||||
late final TextEditingController authDll;
|
||||
late final TextEditingController matchmakingIp;
|
||||
late final Rx<PaneDisplayMode> displayType;
|
||||
late double width;
|
||||
late double height;
|
||||
late double? offsetX;
|
||||
late double? offsetY;
|
||||
Player? player;
|
||||
|
||||
SettingsController() {
|
||||
_storage = GetStorage("settings");
|
||||
|
||||
rebootDll = _createController("reboot", "reboot.dll");
|
||||
consoleDll = _createController("console", "console.dll");
|
||||
craniumDll = _createController("cranium", "cranium.dll");
|
||||
authDll = _createController("cranium2", "craniumv2.dll");
|
||||
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
|
||||
matchmakingIp.addListener(() async {
|
||||
var text = matchmakingIp.text;
|
||||
_storage.write("ip", text);
|
||||
if(await serverConfig.exists()){
|
||||
var config = Config.fromString(await serverConfig.readAsString());
|
||||
if(text.contains(":")){
|
||||
config.set("GameServer", "ip", text.substring(0, text.indexOf(":")));
|
||||
config.set("GameServer", "port", text.substring(text.indexOf(":") + 1));
|
||||
}else {
|
||||
config.set("GameServer", "ip", text);
|
||||
config.set("GameServer", "port", "7777");
|
||||
}
|
||||
|
||||
serverConfig.writeAsString(config.toString());
|
||||
}
|
||||
});
|
||||
|
||||
width = _storage.read("width") ?? window.physicalSize.width;
|
||||
height = _storage.read("height") ?? window.physicalSize.height;
|
||||
offsetX = _storage.read("offset_x");
|
||||
offsetY = _storage.read("offset_y");
|
||||
displayType = Rx(PaneDisplayMode.top);
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, String name) {
|
||||
@@ -47,4 +46,14 @@ class SettingsController extends GetxController {
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
void saveWindowSize() {
|
||||
_storage.write("width", window.physicalSize.width);
|
||||
_storage.write("height", window.physicalSize.height);
|
||||
}
|
||||
|
||||
void saveWindowOffset(Offset position) {
|
||||
_storage.write("offset_x", position.dx);
|
||||
_storage.write("offset_y", position.dy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ class AddLocalVersion extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _gamePathController = TextEditingController();
|
||||
final CheckboxController _injectMemoryFixController = CheckboxController();
|
||||
|
||||
AddLocalVersion({Key? key})
|
||||
: super(key: key);
|
||||
@@ -49,15 +48,6 @@ class AddLocalVersion extends StatelessWidget {
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
SmartCheckBox(
|
||||
controller: _injectMemoryFixController,
|
||||
content: const Text("Inject memory leak fix")
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
@@ -70,10 +60,10 @@ class AddLocalVersion extends StatelessWidget {
|
||||
text: "Save",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_gamePathController.text),
|
||||
memoryFix: _injectMemoryFixController.value
|
||||
location: Directory(_gamePathController.text)
|
||||
));
|
||||
},
|
||||
)
|
||||
|
||||
@@ -12,11 +12,11 @@ import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/build.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/version_name_input.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
|
||||
import '../util/checks.dart';
|
||||
import '../widget/home/build_selector.dart';
|
||||
import '../widget/shared/file_selector.dart';
|
||||
import '../widget/shared/smart_check_box.dart';
|
||||
import 'dialog.dart';
|
||||
|
||||
class AddServerVersion extends StatefulWidget {
|
||||
@@ -31,21 +31,29 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final CheckboxController _injectMemoryFixController = CheckboxController();
|
||||
late Future _future;
|
||||
DownloadStatus _status = DownloadStatus.none;
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
DownloadStatus _status = DownloadStatus.form;
|
||||
String _timeLeft = "00:00:00";
|
||||
double _downloadProgress = 0;
|
||||
String? _error;
|
||||
Process? _manifestDownloadProcess;
|
||||
CancelableOperation? _driveDownloadOperation;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_future = _buildController.builds != null
|
||||
_fetchFuture = _buildController.builds != null
|
||||
? Future.value(true)
|
||||
: compute(fetchBuilds, null)
|
||||
.then((value) => _buildController.builds = value);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
_buildController.addOnBuildChangedListener(() => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -53,6 +61,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_nameController.dispose();
|
||||
_buildController.removeOnBuildChangedListener();
|
||||
_onDisposed();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -80,74 +89,58 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormDialog(
|
||||
content: _createDownloadVersionBody(),
|
||||
buttons: _createDownloadVersionOption(context)
|
||||
);
|
||||
switch(_status){
|
||||
case DownloadStatus.form:
|
||||
return _createFormDialog();
|
||||
case DownloadStatus.downloading:
|
||||
return GenericDialog(
|
||||
header: _createDownloadBody(),
|
||||
buttons: _createCloseButton()
|
||||
);
|
||||
case DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _createExtractingBody(),
|
||||
buttons: _createCloseButton()
|
||||
);
|
||||
case DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception("unknown error"),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => "Cannot download version: $exception"
|
||||
);
|
||||
case DownloadStatus.done:
|
||||
return const InfoDialog(
|
||||
text: "The download was completed successfully!",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<DialogButton> _createDownloadVersionOption(BuildContext context) {
|
||||
switch (_status) {
|
||||
case DownloadStatus.none:
|
||||
return [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: "Download",
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
case DownloadStatus.error:
|
||||
return [
|
||||
DialogButton(
|
||||
type: ButtonType.only,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
)
|
||||
];
|
||||
default:
|
||||
return [
|
||||
DialogButton(
|
||||
text: _status == DownloadStatus.downloading ? "Stop" : "Close",
|
||||
type: ButtonType.only)
|
||||
];
|
||||
}
|
||||
List<DialogButton> _createFormButtons() {
|
||||
return [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: "Download",
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
setState(() => _status = DownloadStatus.downloading);
|
||||
if (_buildController.selectedBuild.hasManifest) {
|
||||
_manifestDownloadProcess = await downloadManifestBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
_pathController.text,
|
||||
_onDownloadProgress
|
||||
);
|
||||
_manifestDownloadProcess!.exitCode
|
||||
.then((value) => _onDownloadComplete());
|
||||
} else {
|
||||
_driveDownloadOperation = CancelableOperation.fromFuture(
|
||||
downloadArchiveBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
_pathController.text,
|
||||
(progress) => _onDownloadProgress(progress, _timeLeft),
|
||||
_onUnrar)
|
||||
).then((_) => _onDownloadComplete(),
|
||||
onError: (error, _) => _handleError(error));
|
||||
}
|
||||
} catch (exception) {
|
||||
_handleError(exception);
|
||||
_manifestDownloadProcess = await downloadManifestBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
_pathController.text,
|
||||
_onDownloadProgress
|
||||
);
|
||||
_manifestDownloadProcess!.exitCode
|
||||
.then((value) => _onDownloadComplete());
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr? _handleError(Object exception) {
|
||||
var message = exception.toString();
|
||||
_onDownloadError(message.contains(":")
|
||||
? " ${message.substring(message.indexOf(":") + 1)}"
|
||||
: message);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onUnrar() {
|
||||
setState(() => _status = DownloadStatus.extracting);
|
||||
}
|
||||
@@ -161,20 +154,20 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
_status = DownloadStatus.done;
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text),
|
||||
memoryFix: _injectMemoryFixController.value
|
||||
location: Directory(_pathController.text)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
void _onDownloadError(String message) {
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.error;
|
||||
_error = message;
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,70 +183,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _createDownloadVersionBody() {
|
||||
return FutureBuilder(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => setState(() => _status = DownloadStatus.error));
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text("Cannot fetch builds: ${snapshot.error}",
|
||||
textAlign: TextAlign.center),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return InfoLabel(
|
||||
label: "Fetching builds...",
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildBody();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
switch (_status) {
|
||||
case DownloadStatus.none:
|
||||
return _createNoneBody();
|
||||
case DownloadStatus.downloading:
|
||||
return _createDownloadBody();
|
||||
case DownloadStatus.extracting:
|
||||
return _createExtractingBody();
|
||||
case DownloadStatus.done:
|
||||
return _createDoneBody();
|
||||
case DownloadStatus.error:
|
||||
return _createErrorBody();
|
||||
}
|
||||
}
|
||||
|
||||
Padding _createErrorBody() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text("An error occurred while downloading:$_error",
|
||||
textAlign: TextAlign.center)),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _createDoneBody() {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text("The download was completed successfully!",
|
||||
textAlign: TextAlign.center)),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _createExtractingBody() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
@@ -263,53 +192,77 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
);
|
||||
}
|
||||
|
||||
Column _createDownloadBody() {
|
||||
Widget _createDownloadBody() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Downloading...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(_manifestDownloadProcess != null)
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Downloading...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createFormDialog() {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) =>
|
||||
_onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return ProgressDialog(
|
||||
text: "Fetching builds and disks...",
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return FormDialog(
|
||||
content: _createFormBody(),
|
||||
buttons: _createFormButtons()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Column _createNoneBody() {
|
||||
Widget _createFormBody() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -327,15 +280,32 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
SmartCheckBox(
|
||||
controller: _injectMemoryFixController,
|
||||
content: const Text("Inject memory leak fix")
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<DialogButton> _createCloseButton() {
|
||||
return [
|
||||
DialogButton(
|
||||
text: "Stop",
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_diskSpace.disks.isEmpty){
|
||||
return;
|
||||
}
|
||||
|
||||
await _fetchFuture;
|
||||
var bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite "
|
||||
"${_buildController.selectedBuild.version.toString()}";
|
||||
_nameController.text = _buildController.selectedBuild.version.toString();
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus { none, downloading, extracting, error, done }
|
||||
enum DownloadStatus { form, downloading, extracting, error, done }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -21,12 +22,20 @@ class GenericDialog extends AbstractDialog {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContentDialog(
|
||||
style: ContentDialogThemeData(
|
||||
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
|
||||
return Stack(
|
||||
children: [
|
||||
MoveWindow(
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
content: header,
|
||||
actions: buttons
|
||||
|
||||
ContentDialog(
|
||||
style: ContentDialogThemeData(
|
||||
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
|
||||
),
|
||||
content: header,
|
||||
actions: buttons
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,19 +140,21 @@ class ProgressDialog extends AbstractDialog {
|
||||
class FutureBuilderDialog extends AbstractDialog {
|
||||
final Future future;
|
||||
final String loadingMessage;
|
||||
final Widget loadedBody;
|
||||
final Widget successfulBody;
|
||||
final Widget unsuccessfulBody;
|
||||
final Function(Object) errorMessageBuilder;
|
||||
final Function()? onError;
|
||||
final bool closeAutomatically;
|
||||
|
||||
const FutureBuilderDialog(
|
||||
{super.key,
|
||||
required this.future,
|
||||
required this.loadingMessage,
|
||||
required this.loadedBody,
|
||||
required this.errorMessageBuilder,
|
||||
this.onError,
|
||||
this.closeAutomatically = false});
|
||||
required this.future,
|
||||
required this.loadingMessage,
|
||||
required this.successfulBody,
|
||||
required this.unsuccessfulBody,
|
||||
required this.errorMessageBuilder,
|
||||
this.onError,
|
||||
this.closeAutomatically = false});
|
||||
|
||||
static Container ofMessage(String message) {
|
||||
return Container(
|
||||
@@ -170,31 +181,43 @@ class FutureBuilderDialog extends AbstractDialog {
|
||||
Widget _createBody(BuildContext context, AsyncSnapshot snapshot){
|
||||
if (snapshot.hasError) {
|
||||
onError?.call();
|
||||
return ofMessage(snapshot.error.toString());
|
||||
return ofMessage(errorMessageBuilder(snapshot.error!));
|
||||
}
|
||||
|
||||
if(snapshot.connectionState == ConnectionState.done && (snapshot.data == null || (snapshot.data is bool && !snapshot.data))){
|
||||
return unsuccessfulBody;
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return InfoLabel(
|
||||
label: loadingMessage,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()),
|
||||
);
|
||||
return _createLoadingBody();
|
||||
}
|
||||
|
||||
if(closeAutomatically){
|
||||
Navigator.of(context).pop(true);
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => Navigator.of(context).pop(true));
|
||||
return _createLoadingBody();
|
||||
}
|
||||
|
||||
return loadedBody;
|
||||
return successfulBody;
|
||||
}
|
||||
|
||||
InfoLabel _createLoadingBody() {
|
||||
return InfoLabel(
|
||||
label: loadingMessage,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()),
|
||||
);
|
||||
}
|
||||
|
||||
DialogButton _createButton(BuildContext context, AsyncSnapshot snapshot){
|
||||
return DialogButton(
|
||||
text: snapshot.hasData || snapshot.hasError ? "Close" : "Stop",
|
||||
type: ButtonType.only,
|
||||
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
|
||||
text: snapshot.hasData
|
||||
|| snapshot.hasError
|
||||
|| (snapshot.connectionState == ConnectionState.done && snapshot.data == null) ? "Close" : "Stop",
|
||||
type: ButtonType.only,
|
||||
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ Future<void> showBrokenError() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "The lawin server is not working correctly"
|
||||
text: "The backend server is not working correctly"
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -20,20 +20,26 @@ Future<void> showMissingDllError(String name) async {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTokenError() async {
|
||||
Future<void> showTokenErrorFixable() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "A token error occurred, restart the game and the lawin server, then try again"
|
||||
text: "A token error occurred. "
|
||||
"The backend server has been automatically restarted to fix the issue. "
|
||||
"Relaunch your game to check if the issue has been automatically fixed. "
|
||||
"Otherwise, open an issue on Discord."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showUnsupportedHeadless() async {
|
||||
Future<void> showTokenErrorUnfixable() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "This version of Fortnite doesn't support headless hosting"
|
||||
text: "A token error occurred. "
|
||||
"This issue cannot be resolved automatically as the server isn't embedded."
|
||||
"Please restart the server manually, then relaunch your game to check if the issue has been fixed. "
|
||||
"Otherwise, open an issue on Discord."
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/embedded/server.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/future.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../util/server.dart';
|
||||
@@ -14,26 +17,31 @@ import '../util/server.dart';
|
||||
extension ServerControllerDialog on ServerController {
|
||||
static Semaphore semaphore = Semaphore();
|
||||
|
||||
Future<bool> changeStateInteractive(bool onlyIfNeeded, [bool isRetry = false]) async {
|
||||
Future<bool> start({required bool required, required bool askPortKill, bool isRetry = false}) async {
|
||||
try{
|
||||
semaphore.acquire();
|
||||
if (type() == ServerType.local) {
|
||||
return _checkLocalServerInteractive(onlyIfNeeded);
|
||||
return _pingSelfInteractive(required);
|
||||
}
|
||||
|
||||
var oldStarted = started();
|
||||
if(oldStarted && onlyIfNeeded){
|
||||
if(oldStarted && required){
|
||||
return true;
|
||||
}
|
||||
|
||||
started.value = !started.value;
|
||||
return await _doStateChange(oldStarted, onlyIfNeeded, isRetry);
|
||||
var result = await _startInternal(oldStarted, required, askPortKill, isRetry);
|
||||
if(!result){
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _pingSelfInteractive(true);
|
||||
}finally{
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _doStateChange(bool oldStarted, bool onlyIfNeeded, bool isRetry) async {
|
||||
Future<bool> _startInternal(bool oldStarted, bool required, bool askPortKill, bool isRetry) async {
|
||||
if (oldStarted) {
|
||||
var result = await stop();
|
||||
if (!result) {
|
||||
@@ -45,13 +53,14 @@ extension ServerControllerDialog on ServerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await start(!onlyIfNeeded);
|
||||
if(result.type == ServerResultType.ignoreStart) {
|
||||
var conditions = await checkServerPreconditions(host.text, port.text, type.value, !required);
|
||||
var result = conditions.type == ServerResultType.canStart ? await _startServer(required) : conditions;
|
||||
if(result.type == ServerResultType.alreadyStarted) {
|
||||
started.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var handled = await _handleResultType(oldStarted, onlyIfNeeded, isRetry, result);
|
||||
var handled = await _handleResultType(oldStarted, required, isRetry, askPortKill, result);
|
||||
if (!handled) {
|
||||
started.value = false;
|
||||
return false;
|
||||
@@ -60,7 +69,42 @@ extension ServerControllerDialog on ServerController {
|
||||
return handled;
|
||||
}
|
||||
|
||||
Future<bool> _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, ServerResult result) async {
|
||||
Future<ServerResult> _startServer(bool closeAutomatically) async {
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
embeddedServer = await startEmbeddedServer(
|
||||
() => Get.find<SettingsController>().matchmakingIp.text,
|
||||
);
|
||||
embeddedMatchmaker = await startEmbeddedMatchmaker();
|
||||
break;
|
||||
case ServerType.remote:
|
||||
var uriResult = await _pingRemoteInteractive(closeAutomatically);
|
||||
if(uriResult == null){
|
||||
return ServerResult(
|
||||
type: ServerResultType.cannotPingServer
|
||||
);
|
||||
}
|
||||
|
||||
remoteServer = await startRemoteServer(uriResult);
|
||||
break;
|
||||
case ServerType.local:
|
||||
break;
|
||||
}
|
||||
}catch(error, stackTrace){
|
||||
return ServerResult(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
type: ServerResultType.unknownError
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
type: ServerResultType.canStart
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, bool askPortKill, ServerResult result) async {
|
||||
switch (result.type) {
|
||||
case ServerResultType.missingHostError:
|
||||
_showMissingHostError();
|
||||
@@ -72,11 +116,6 @@ extension ServerControllerDialog on ServerController {
|
||||
_showIllegalPortError();
|
||||
return false;
|
||||
case ServerResultType.cannotPingServer:
|
||||
if(!started() || result.pid != embeddedServer?.pid){
|
||||
return false;
|
||||
}
|
||||
|
||||
_showPingErrorDialog();
|
||||
return false;
|
||||
case ServerResultType.portTakenError:
|
||||
if (isRetry) {
|
||||
@@ -84,24 +123,15 @@ extension ServerControllerDialog on ServerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await _showPortTakenDialog();
|
||||
if (!result) {
|
||||
return false;
|
||||
if(askPortKill) {
|
||||
var result = await _showPortTakenDialog();
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await freeLawinPort();
|
||||
return _doStateChange(oldStarted, onlyIfNeeded, true);
|
||||
case ServerResultType.serverDownloadRequiredError:
|
||||
if (isRetry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await downloadServerInteractive(false);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _doStateChange(oldStarted, onlyIfNeeded, true);
|
||||
return _startInternal(oldStarted, onlyIfNeeded, askPortKill, true);
|
||||
case ServerResultType.unknownError:
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
@@ -114,39 +144,67 @@ extension ServerControllerDialog on ServerController {
|
||||
)
|
||||
);
|
||||
return false;
|
||||
case ServerResultType.ignoreStart:
|
||||
case ServerResultType.started:
|
||||
return true;
|
||||
case ServerResultType.alreadyStarted:
|
||||
case ServerResultType.canStart:
|
||||
return true;
|
||||
case ServerResultType.stopped:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _checkLocalServerInteractive(bool ignorePrompts) async {
|
||||
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
|
||||
try {
|
||||
var future = pingSelf(port.text);
|
||||
if(!ignorePrompts) {
|
||||
await showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: future,
|
||||
loadingMessage: "Pinging server...",
|
||||
loadedBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} works correctly"),
|
||||
errorMessageBuilder: (
|
||||
exception) => "An error occurred while pining the server: $exception"
|
||||
)
|
||||
);
|
||||
}
|
||||
return await future != null;
|
||||
return await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: Future.wait([
|
||||
pingSelf(port.text),
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
]),
|
||||
loadingMessage: "Pinging ${type().id} server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The ${type().id} server works correctly"),
|
||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
|
||||
errorMessageBuilder: (
|
||||
exception) => "An error occurred while pining the ${type().id} server: $exception",
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
) ?? false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri?> _pingRemoteInteractive(bool closeAutomatically) async {
|
||||
try {
|
||||
var mainFuture = ping(host.text, port.text);
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: Future.wait([
|
||||
mainFuture,
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
]),
|
||||
loadingMessage: "Pinging remote server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} works correctly"),
|
||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} doesn't work. Check the hostname and/or the port and try again."),
|
||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception",
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
) ?? false;
|
||||
return result ? await mainFuture : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showPortTakenError() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
@@ -178,20 +236,6 @@ extension ServerControllerDialog on ServerController {
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
void _showPingErrorDialog() {
|
||||
if(!started.value){
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
const InfoDialog(
|
||||
text: "The lawin server is not working correctly. Check the configuration in the associated tab and try again."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _showCannotStopError() {
|
||||
if(!started.value){
|
||||
return;
|
||||
@@ -201,47 +245,45 @@ extension ServerControllerDialog on ServerController {
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
const InfoDialog(
|
||||
text: "Cannot stop lawin server"
|
||||
text: "Cannot stop backend server"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void showUnexpectedError() {
|
||||
void showUnexpectedServerError() {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
const InfoDialog(
|
||||
text: "The lawin server died unexpectedly"
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The backend server died unexpectedly",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: "Close",
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: "Open log",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
launchUrl(serverLogFile.uri);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _showIllegalPortError() {
|
||||
showMessage("Illegal port for lawin server, use only numbers");
|
||||
showMessage("Illegal port for backend server, use only numbers");
|
||||
}
|
||||
|
||||
void _showMissingPortError() {
|
||||
showMessage("Missing port for lawin server");
|
||||
showMessage("Missing port for backend server");
|
||||
}
|
||||
|
||||
void _showMissingHostError() {
|
||||
showMessage("Missing the host name for lawin server");
|
||||
showMessage("Missing the host name for backend server");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> downloadServerInteractive(bool closeAutomatically) async {
|
||||
var download = compute(downloadServer, true);
|
||||
return await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: download,
|
||||
loadingMessage: "Downloading server...",
|
||||
loadedBody: FutureBuilderDialog.ofMessage(
|
||||
"The server was downloaded successfully"),
|
||||
errorMessageBuilder: (
|
||||
message) => "Cannot download server: $message",
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
) ?? download.isCompleted();
|
||||
}
|
||||
333
lib/src/embedded/auth.dart
Normal file
333
lib/src/embedded/auth.dart
Normal file
@@ -0,0 +1,333 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
|
||||
final Directory _profiles = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\profiles");
|
||||
|
||||
const String _token = "reboot_token";
|
||||
const String _clientId = "reboot_client";
|
||||
const String _device = "reboot_device";
|
||||
const String _sessionId = "3c3662bcb661d6de679c636744c66b62";
|
||||
|
||||
List<Map<String, Object>> getAccounts(Context context) {
|
||||
return context.query.getList("accountId").map(getAccount).toList();
|
||||
}
|
||||
|
||||
Map<String, Object> getAccount(String account) {
|
||||
return {"id": account, "displayName": _parseUsername(account), "externalAuths": {}};
|
||||
}
|
||||
|
||||
Map<String, Object> getAccountInfo(Context context) {
|
||||
var usernameId = context.pathParams.get("accountId")!;
|
||||
var accountName = _parseUsername(usernameId);
|
||||
return {
|
||||
"id": usernameId,
|
||||
"displayName": accountName,
|
||||
"name": "Reboot",
|
||||
"email": usernameId,
|
||||
"failedLoginAttempts": 0,
|
||||
"lastLogin": "2022-11-08T18:55:52.341Z",
|
||||
"numberOfDisplayNameChanges": 0,
|
||||
"ageGroup": "UNKNOWN",
|
||||
"headless": false,
|
||||
"country": "US",
|
||||
"lastName": "Server",
|
||||
"preferredLanguage": "en",
|
||||
"canUpdateDisplayName": false,
|
||||
"tfaEnabled": false,
|
||||
"emailVerified": true,
|
||||
"minorVerified": false,
|
||||
"minorExpected": false,
|
||||
"minorStatus": "UNKNOWN"
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getExternalAuths(Context context) => [];
|
||||
|
||||
Future<Map<String, Object>> getOAuthToken(Context context) async {
|
||||
var usernameId = await _getUsername(context);
|
||||
var accountName = _parseUsername(usernameId);
|
||||
return {
|
||||
"access_token": _token,
|
||||
"expires_in": 28800,
|
||||
"expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"token_type": "bearer",
|
||||
"refresh_token": _token,
|
||||
"refresh_expires": 86400,
|
||||
"refresh_expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"account_id": usernameId,
|
||||
"client_id": _clientId,
|
||||
"internal_client": true,
|
||||
"client_service": "fortnite",
|
||||
"displayName": accountName,
|
||||
"app": "fortnite",
|
||||
"in_app_id": usernameId,
|
||||
"device_id": _device
|
||||
};
|
||||
}
|
||||
|
||||
Future<String> _getUsername(Context context) async {
|
||||
var params = await parseBody(context);
|
||||
var username = params["username"];
|
||||
return username ?? "unknown@projectreboot.dev";
|
||||
}
|
||||
|
||||
Map<String, Object> verifyOAuthToken(Context context) {
|
||||
return {
|
||||
"token": _token,
|
||||
"session_id": _sessionId,
|
||||
"token_type": "bearer",
|
||||
"client_id": _clientId,
|
||||
"internal_client": true,
|
||||
"client_service": "fortnite",
|
||||
"account_id": "unknown",
|
||||
"expires_in": 28800,
|
||||
"expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"auth_method": "exchange_code",
|
||||
"display_name": "unknown",
|
||||
"app": "fortnite",
|
||||
"in_app_id": "unknown",
|
||||
"device_id": _device
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getExchange(Context context) => [];
|
||||
|
||||
List<String> getSsoDomains(Context context) => [
|
||||
"unrealengine.com",
|
||||
"unrealtournament.com",
|
||||
"fortnite.com",
|
||||
"epicgames.com"
|
||||
];
|
||||
|
||||
String tryPlayOnPlatform(Context context) => "true";
|
||||
|
||||
List<Map<String, Object>> getFeatures(Context context) => [];
|
||||
|
||||
Map<String, Object?> getProfile(Context context){
|
||||
var profileId = context.query.get("profileId");
|
||||
if (profileId == null) {
|
||||
return {"Error": "Profile not defined."};
|
||||
}
|
||||
|
||||
var profileJson = _getProfileJson(profileId, context);
|
||||
var profileFile = _getProfileFile(context);
|
||||
var baseRevision = profileJson["rvn"] ?? 0;
|
||||
var queryRevision = context.query.getInt("rvn") ?? -1;
|
||||
var profileChanges = _getFullProfileUpdate(context, profileId, profileJson, queryRevision, baseRevision);
|
||||
if(profileId == "athena" && !profileFile.existsSync()) {
|
||||
profileFile.writeAsStringSync(json.encode(profileJson), flush: true);
|
||||
}
|
||||
|
||||
return {
|
||||
"profileRevision": baseRevision,
|
||||
"profileId": profileId,
|
||||
"profileChangesBaseRevision": baseRevision,
|
||||
"profileChanges": profileChanges,
|
||||
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
|
||||
"serverTime": "2022-11-08T18:55:52.341Z",
|
||||
"responseVersion": 1
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getProfileJson(String profileId, Context context) {
|
||||
if(profileId == "athena"){
|
||||
var profile = _getProfileFile(context);
|
||||
if(profile.existsSync()){
|
||||
return json.decode(profile.readAsStringSync());
|
||||
}
|
||||
|
||||
var body = loadEmbedded("profiles/$profileId.json").readAsStringSync();
|
||||
return json.decode(body);
|
||||
}
|
||||
|
||||
var profileJson = json.decode(loadEmbedded("profiles/$profileId.json").readAsStringSync());
|
||||
return profileJson;
|
||||
}
|
||||
|
||||
Future<Map<String, Object>> equipItem(Context context) async {
|
||||
var profileFile = _getProfileFile(context);
|
||||
var profileJson = json.decode(profileFile.readAsStringSync());
|
||||
var baseRevision = profileJson["rvn"] ?? 0;
|
||||
var queryRevision = context.query.getInt("rvn") ?? -1;
|
||||
|
||||
var body = json.decode(utf8.decode(await context.body));
|
||||
var variant = _getReturnVariant(body, profileJson);
|
||||
var change = _getStatsChanged(body, profileJson);
|
||||
var profileChanges = _getProfileChanges(queryRevision, baseRevision, profileJson, change, body, variant);
|
||||
profileFile.writeAsStringSync(json.encode(profileJson));
|
||||
return {
|
||||
"profileRevision": baseRevision,
|
||||
"profileId": "athena",
|
||||
"profileChangesBaseRevision": baseRevision,
|
||||
"profileChanges": profileChanges,
|
||||
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
|
||||
"serverTime": "2022-11-08T18:55:52.341Z",
|
||||
"responseVersion": 1
|
||||
};
|
||||
}
|
||||
|
||||
List<dynamic> _getProfileChanges(int queryRevision, baseRevision, profileJson, bool change, body, bool variant) {
|
||||
var changes = [];
|
||||
if (change) {
|
||||
var category = ("favorite_${body["slotName"] ?? "character"}")
|
||||
.toLowerCase();
|
||||
if (category == "favorite_itemwrap") {
|
||||
category += "s";
|
||||
}
|
||||
|
||||
profileJson["rvn"] = (profileJson["rvn"] ?? 0) + 1;
|
||||
profileJson["commandRevision"] = (profileJson["commandRevision"] ?? 0) + 1;
|
||||
|
||||
changes.add({
|
||||
"changeType": "statModified",
|
||||
"name": category,
|
||||
"value": profileJson["stats"]["attributes"][category]
|
||||
});
|
||||
if (variant) {
|
||||
changes.add({
|
||||
"changeType": "itemAttrChanged",
|
||||
"itemId": body["itemToSlot"],
|
||||
"attributeName": "variants",
|
||||
"attributeValue": profileJson["items"][body["itemToSlot"]]["attributes"]["variants"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(queryRevision != baseRevision){
|
||||
return [{
|
||||
"changeType": "fullProfileUpdate",
|
||||
"profile": profileJson
|
||||
}];
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
bool _getStatsChanged(body, profileJson) {
|
||||
var slotName = body["slotName"];
|
||||
if (slotName == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (slotName) {
|
||||
case "Character":
|
||||
profileJson["stats"]["attributes"]["favorite_character"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Backpack":
|
||||
profileJson["stats"]["attributes"]["favorite_backpack"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Pickaxe":
|
||||
profileJson["stats"]["attributes"]["favorite_pickaxe"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Glider":
|
||||
profileJson["stats"]["attributes"]["favorite_glider"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "SkyDiveContrail":
|
||||
profileJson["stats"]["attributes"]["favorite_skydivecontrail"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "MusicPack":
|
||||
profileJson["stats"]["attributes"]["favorite_musicpack"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "LoadingScreen":
|
||||
profileJson["stats"]["attributes"]["favorite_loadingscreen"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Dance":
|
||||
var index = body["indexWithinSlot"] ?? 0;
|
||||
if (index >= 0) {
|
||||
profileJson["stats"]["attributes"]["favorite_dance"][index] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
|
||||
return true;
|
||||
case "ItemWrap":
|
||||
var index = body["indexWithinSlot"] ?? 0;
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < 7; i++) {
|
||||
profileJson["stats"]["attributes"]["favorite_itemwraps"][i] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
} else {
|
||||
profileJson["stats"]["attributes"]["favorite_itemwraps"][index] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _getReturnVariant(body, profileJson) {
|
||||
var variantUpdates = body["variantUpdates"] ?? [];
|
||||
if(!variantUpdates.toString().contains("active")){
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
var variantJson = profileJson["items"][body["itemToSlot"]]["attributes"]["variants"] ?? [];
|
||||
if (variantJson.isEmpty) {
|
||||
variantJson = variantUpdates;
|
||||
}
|
||||
|
||||
for (var i in variantJson) {
|
||||
try {
|
||||
if (variantJson[i]["channel"].toLowerCase() == body["variantUpdates"][i]["channel"].toLowerCase()) {
|
||||
profileJson["items"][body["itemToSlot"]]["attributes"]["variants"][i]["active"] = body["variantUpdates"][i]["active"] ?? "";
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
// Ignored
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> _getFullProfileUpdate(Context context, String profileName, Map<String, dynamic> profileJson, int queryRevision, int baseRevision) {
|
||||
if (queryRevision == baseRevision) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (profileName == "athena") {
|
||||
var season = parseSeason(context);
|
||||
profileJson["stats"]["attributes"]["season_num"] = season;
|
||||
profileJson["stats"]["attributes"]["book_purchased"] = true;
|
||||
profileJson["stats"]["attributes"]["book_level"] = 100;
|
||||
profileJson["stats"]["attributes"]["season_match_boost"] = 100;
|
||||
profileJson["stats"]["attributes"]["season_friend_match_boost"] = 100;
|
||||
}
|
||||
|
||||
return [{
|
||||
"changeType": "fullProfileUpdate",
|
||||
"profile": profileJson
|
||||
}];
|
||||
}
|
||||
|
||||
String _parseUsername(String username) =>
|
||||
username.contains("@") ? username.split("@")[0] : username;
|
||||
|
||||
File _getProfileFile(Context context) {
|
||||
if(!_profiles.existsSync()){
|
||||
_profiles.createSync(recursive: true);
|
||||
}
|
||||
|
||||
return File("${_profiles.path}\\ClientProfile-${parseSeasonBuild(context)}.json");
|
||||
}
|
||||
|
||||
36
lib/src/embedded/error.dart
Normal file
36
lib/src/embedded/error.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
class EmbeddedErrorWriter extends ErrorWriter {
|
||||
static const String _errorName = "errors.com.lawinserver.common.not_found";
|
||||
static const String _errorCode = "1004";
|
||||
|
||||
@override
|
||||
FutureOr<Response> make404(Context ctx) {
|
||||
stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}");
|
||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||
return Response.json(
|
||||
statusCode: 204,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
|
||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||
return Response(
|
||||
statusCode: 500,
|
||||
body: {
|
||||
"errorCode": _errorName,
|
||||
"errorMessage": "Sorry the resource you were trying to find could not be found",
|
||||
"numericErrorCode": _errorCode,
|
||||
"originatingService": "any",
|
||||
"intent": "prod"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
33
lib/src/embedded/lightswitch.dart
Normal file
33
lib/src/embedded/lightswitch.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
Map<String, Object?> getFortniteStatus(Context context) => {
|
||||
"serviceInstanceId": "fortnite",
|
||||
"status": "UP",
|
||||
"message": "Fortnite is online",
|
||||
"maintenanceUri": null,
|
||||
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
|
||||
"allowedActions": [],
|
||||
"banned": false,
|
||||
"launcherInfoDTO": {
|
||||
"appName": "Fortnite",
|
||||
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
|
||||
"namespace": "fn"
|
||||
}
|
||||
};
|
||||
|
||||
List<Map<String, Object?>> getBulkStatus(Context context) => [
|
||||
{
|
||||
"serviceInstanceId": "fortnite",
|
||||
"status": "UP",
|
||||
"message": "fortnite is up.",
|
||||
"maintenanceUri": null,
|
||||
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
|
||||
"allowedActions": ["PLAY", "DOWNLOAD"],
|
||||
"banned": false,
|
||||
"launcherInfoDTO": {
|
||||
"appName": "Fortnite",
|
||||
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
|
||||
"namespace": "fn"
|
||||
}
|
||||
}
|
||||
];
|
||||
144
lib/src/embedded/matchmaking.dart
Normal file
144
lib/src/embedded/matchmaking.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
String _build = "0";
|
||||
String? _customIp;
|
||||
|
||||
Map<String, Object> getPlayerTicket(Context context){
|
||||
var bucketId = context.query.get("bucketId");
|
||||
if(bucketId == null){
|
||||
return {"Error": "Missing bucket id"};
|
||||
}
|
||||
|
||||
_build = bucketId.split(":")[0];
|
||||
_customIp = context.query.get("player.option.customKey");
|
||||
return {
|
||||
"serviceUrl": "ws://127.0.0.1:8080",
|
||||
"ticketType": "mms-player",
|
||||
"payload": "69=",
|
||||
"signature": "420="
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, Object?> getSessionAccount(Context context) => {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"sessionId": context.pathParams.get("sessionId"),
|
||||
"key": "AOJEv8uTFmUh7XM2328kq9rlAzeQ5xzWzPIiyKn2s7s="
|
||||
};
|
||||
|
||||
Future<Map<String, Object?>> getMatch(Context context, String Function() ipQuery) async {
|
||||
var ipAndPort = _customIp ?? ipQuery().trim();
|
||||
var ip = ipAndPort.contains(":") ? ipAndPort.split(":")[0] : ipAndPort;
|
||||
var port = ipAndPort.contains(":") ? int.parse(ipAndPort.split(":")[1]) : 7777;
|
||||
return {
|
||||
"id": context.pathParams.get("sessionId"),
|
||||
"ownerId": _randomUUID(),
|
||||
"ownerName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
|
||||
"serverName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
|
||||
"serverAddress": ip,
|
||||
"serverPort": port,
|
||||
"maxPublicPlayers": 220,
|
||||
"openPublicPlayers": 175,
|
||||
"maxPrivatePlayers": 0,
|
||||
"openPrivatePlayers": 0,
|
||||
"attributes": {
|
||||
"REGION_s": "EU",
|
||||
"GAMEMODE_s": "FORTATHENA",
|
||||
"ALLOWBROADCASTING_b": true,
|
||||
"SUBREGION_s": "GB",
|
||||
"DCID_s": "FORTNITE-LIVEEUGCEC1C2E30UBRCORE0A-14840880",
|
||||
"tenant_s": "Fortnite",
|
||||
"MATCHMAKINGPOOL_s": "Any",
|
||||
"STORMSHIELDDEFENSETYPE_i": 0,
|
||||
"HOTFIXVERSION_i": 0,
|
||||
"PLAYLISTNAME_s": "Playlist_DefaultSolo",
|
||||
"SESSIONKEY_s": _randomUUID(),
|
||||
"TENANT_s": "Fortnite",
|
||||
"BEACONPORT_i": 15009
|
||||
},
|
||||
"publicPlayers": [],
|
||||
"privatePlayers": [],
|
||||
"totalPlayers": 45,
|
||||
"allowJoinInProgress": false,
|
||||
"shouldAdvertise": false,
|
||||
"isDedicated": false,
|
||||
"usesStats": false,
|
||||
"allowInvites": false,
|
||||
"usesPresence": false,
|
||||
"allowJoinViaPresence": true,
|
||||
"allowJoinViaPresenceFriendsOnly": false,
|
||||
"buildUniqueId": _build,
|
||||
"lastUpdated": "2022-11-08T18:55:52.341Z",
|
||||
"started": false
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getMatchmakingRequests() => [];
|
||||
|
||||
void queueMatchmaking(WebSocket ws) {
|
||||
var now = DateTime.now();
|
||||
var ticketId = md5.convert(utf8.encode("1$now")).toString();
|
||||
var matchId = md5.convert(utf8.encode("2$now")).toString();
|
||||
var sessionId = md5.convert(utf8.encode("3$now")).toString();
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"state": "Connecting"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"totalPlayers": 1,
|
||||
"connectedPlayers": 1,
|
||||
"state": "Waiting"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"ticketId": ticketId,
|
||||
"queuedPlayers": 0,
|
||||
"estimatedWaitSec": 0,
|
||||
"status": {},
|
||||
"state": "Queued"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"matchId": matchId,
|
||||
"state": "SessionAssignment"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"matchId": matchId,
|
||||
"sessionId": sessionId,
|
||||
"joinDelaySec": 1
|
||||
},
|
||||
"name": "Play"
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
String _randomUUID() => const Uuid().v4().replaceAll("-", "").toUpperCase();
|
||||
1205
lib/src/embedded/misc.dart
Normal file
1205
lib/src/embedded/misc.dart
Normal file
File diff suppressed because it is too large
Load Diff
16
lib/src/embedded/privacy.dart
Normal file
16
lib/src/embedded/privacy.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
Map<String, Object?> getPrivacy(Context context) => {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"optOutOfPublicLeaderboards": false
|
||||
};
|
||||
|
||||
|
||||
Future<Map<String, Object?>> postPrivacy(Context context) async {
|
||||
var body = await parseBody(context);
|
||||
return {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"optOutOfPublicLeaderboards": body["optOutOfPublicLeaderboards"]
|
||||
};
|
||||
}
|
||||
138
lib/src/embedded/server.dart
Normal file
138
lib/src/embedded/server.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import "package:jaguar/jaguar.dart";
|
||||
import "package:reboot_launcher/src/embedded/auth.dart";
|
||||
import 'package:reboot_launcher/src/embedded/misc.dart';
|
||||
import 'package:reboot_launcher/src/embedded/privacy.dart';
|
||||
import "package:reboot_launcher/src/embedded/storage.dart";
|
||||
import 'package:reboot_launcher/src/embedded/storefront.dart';
|
||||
import "package:reboot_launcher/src/embedded/version.dart";
|
||||
|
||||
import '../util/server.dart';
|
||||
import "error.dart";
|
||||
import "lightswitch.dart";
|
||||
import 'matchmaking.dart';
|
||||
|
||||
bool _loggingCapabilities = false;
|
||||
|
||||
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
|
||||
var server = _createServer(ipQuery);
|
||||
await server.serve(logRequests: true);
|
||||
return server;
|
||||
}
|
||||
|
||||
Future<Jaguar> startEmbeddedMatchmaker() async {
|
||||
var server = _createMatchmaker();
|
||||
server.serve(logRequests: true);
|
||||
return server;
|
||||
}
|
||||
|
||||
Jaguar _createServer(String Function() ipQuery) {
|
||||
var server = Jaguar(address: "127.0.0.1", port: 3551, errorWriter: EmbeddedErrorWriter());
|
||||
|
||||
// Version
|
||||
server.getJson("/fortnite/api/version", getVersion);
|
||||
server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate);
|
||||
server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate);
|
||||
server.getJson("/fortnite/api/versioncheck*", hasUpdate);
|
||||
|
||||
// Auth
|
||||
server.getJson("/account/api/public/account/displayName/:accountId", getAccountInfo);
|
||||
server.getJson("/account/api/public/account/:accountId", getAccountInfo);
|
||||
server.getJson("/account/api/public/account/:accountId/externalAuths", getExternalAuths);
|
||||
server.getJson("/account/api/public/account", getAccounts);
|
||||
server.delete("/account/api/oauth/sessions/kill/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/account/api/oauth/verify", verifyOAuthToken);
|
||||
server.postJson("/account/api/oauth/token", getOAuthToken);
|
||||
server.postJson("/account/api/oauth/exchange", getExchange);
|
||||
server.getJson("/account/api/epicdomains/ssodomains", getSsoDomains);
|
||||
server.post("/fortnite/api/game/v2/tryPlayOnPlatform/account/*", tryPlayOnPlatform);
|
||||
server.post("/datarouter/api/v1/public/data/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/fortnite/api/game/v2/enabled_features", getFeatures);
|
||||
server.postJson("/fortnite/api/game/v2/grant_access/*", (context) => Response(statusCode: 204));
|
||||
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/EquipBattleRoyaleCustomization", equipItem);
|
||||
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/*", getProfile);
|
||||
|
||||
// Storage
|
||||
server.getJson("/fortnite/api/cloudstorage/system", getStorageSettings);
|
||||
server.get("/fortnite/api/cloudstorage/system/:file", getStorageSetting);
|
||||
server.getJson("/fortnite/api/cloudstorage/user/:accountId", getStorageAccount);
|
||||
server.getJson("/fortnite/api/cloudstorage/user/:accountId/:file", getStorageFile);
|
||||
server.put("/fortnite/api/cloudstorage/user/:accountId/:file", addStorageFile);
|
||||
|
||||
// Status
|
||||
server.getJson("/lightswitch/api/service/Fortnite/status", getFortniteStatus);
|
||||
server.getJson("/lightswitch/api/service/bulk/status", getBulkStatus);
|
||||
|
||||
// Keychain and catalog
|
||||
server.get("/fortnite/api/storefront/v2/catalog", getCatalog);
|
||||
server.get("/fortnite/api/storefront/v2/keychain", getKeyChain);
|
||||
server.get("/catalog/api/shared/bulk/offers", getOffers);
|
||||
|
||||
// Matchmaking
|
||||
server.get("/fortnite/api/matchmaking/session/findPlayer/*", (context) => Response(statusCode: 200));
|
||||
server.getJson("/fortnite/api/game/v2/matchmakingservice/ticket/player/*", getPlayerTicket);
|
||||
server.getJson("/fortnite/api/game/v2/matchmaking/account/:accountId/session/:sessionId", getSessionAccount);
|
||||
server.getJson("/fortnite/api/matchmaking/session/:sessionId", (context) => getMatch(context, ipQuery));
|
||||
server.post("/fortnite/api/matchmaking/session/:accountId/join", (context) => Response(statusCode: 204));
|
||||
server.postJson("/fortnite/api/matchmaking/session/matchMakingRequest", (context) => getMatchmakingRequests);
|
||||
|
||||
// Misc
|
||||
server.getJson("/api/v1/events/Fortnite/download/*", getDownload);
|
||||
server.getJson("/fortnite/api/receipts/v1/account/:accountId/receipts", getReceipts);
|
||||
server.getJson("/content/api/pages/*", getContentPages);
|
||||
server.getJson("/friends/api/v1/:accountId/settings", getFriendsSettings);
|
||||
server.getJson("/friends/api/v1/:accountId/blocklist", getFriendsBlocklist);
|
||||
server.getJson("/friends/api/public/blocklist/:accountId", getFriendsBlocklist);
|
||||
server.getJson("/friends/api/public/friends/:accountId", getFriendsList);
|
||||
server.getJson("/friends/api/public/list/fortnite/:accountId/recentPlayers", getRecentPlayers);
|
||||
server.getJson("/fortnite/api/calendar/v1/timeline", getTimeline);
|
||||
server.getJson("/fortnite/api/game/v2/events/tournamentandhistory/:accountId/EU/WindowsClient", getTournamentHistory);
|
||||
server.get("/waitingroom/api/waitingroom", (context) => Response(statusCode: 204));
|
||||
server.postJson("/api/v1/user/setting", (context) => []);
|
||||
server.getJson("/eulatracking/api/public/agreements/fn/account/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/socialban/api/public/v1/:accountId", getSocialBan);
|
||||
server.getJson("/party/api/v1/Fortnite/user/*", getParty);
|
||||
server.getJson("/friends/api/v1/*/settings", (context) => {});
|
||||
server.getJson("/friends/api/v1/*/blocklist", (context) => {});
|
||||
server.getJson("/friends/api/public/friends", (context) => []);
|
||||
server.getJson("/friends/api/v1/:accountId/summary", (context) => []);
|
||||
server.getJson("/friends/api/public/list/fortnite/*/recentPlayers", (context) => []);
|
||||
server.getJson("/friends/api/public/blocklist/*", getBlockedFriends);
|
||||
|
||||
// Privacy
|
||||
server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy);
|
||||
server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
|
||||
|
||||
return _addLoggingCapabilities(server);
|
||||
}
|
||||
Jaguar _createMatchmaker(){
|
||||
var server = Jaguar(address: "127.0.0.1", port: 8080);
|
||||
WebSocket? ws;
|
||||
server.wsStream(
|
||||
"/",
|
||||
(_, input) => ws = input,
|
||||
after: [(_) => queueMatchmaking(ws!)]
|
||||
);
|
||||
return _addLoggingCapabilities(server);
|
||||
}
|
||||
|
||||
Jaguar _addLoggingCapabilities(Jaguar server) {
|
||||
if(_loggingCapabilities){
|
||||
return server;
|
||||
}
|
||||
|
||||
server.log.onRecord.listen((line) {
|
||||
stdout.writeln(line);
|
||||
serverLogFile.writeAsString("$line\n", mode: FileMode.append);
|
||||
});
|
||||
|
||||
server.onException.add((ctx, exception, trace) {
|
||||
stderr.writeln("An error occurred: $exception");
|
||||
serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append);
|
||||
});
|
||||
|
||||
_loggingCapabilities = true;
|
||||
return server;
|
||||
}
|
||||
111
lib/src/embedded/storage.dart
Normal file
111
lib/src/embedded/storage.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
|
||||
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
|
||||
|
||||
const String _engineName = "DefaultEngine.ini";
|
||||
final String _engineIni = loadEmbedded("config/$_engineName").readAsStringSync();
|
||||
|
||||
const String _gameName = "DefaultGame.ini";
|
||||
final String _gameIni = loadEmbedded("config/$_gameName").readAsStringSync();
|
||||
|
||||
const String _runtimeName = "DefaultRuntimeOptions.ini";
|
||||
final String _runtimeIni = loadEmbedded("config/$_runtimeName").readAsStringSync();
|
||||
|
||||
List<Map<String, Object>> getStorageSettings(Context context) => [
|
||||
_getStorageSetting(_engineName, _engineIni),
|
||||
_getStorageSetting(_gameName, _gameIni),
|
||||
_getStorageSetting(_runtimeName, _runtimeIni)
|
||||
];
|
||||
|
||||
Map<String, Object> _getStorageSetting(String name, String source){
|
||||
var bytes = utf8.encode(source);
|
||||
return {
|
||||
"uniqueFilename": name,
|
||||
"filename": name,
|
||||
"hash": sha1.convert(bytes).toString(),
|
||||
"hash256": sha256.convert(bytes).toString(),
|
||||
"length": bytes.length,
|
||||
"contentType": "application/octet-stream",
|
||||
"uploaded": "2020-02-23T18:35:53.967Z",
|
||||
"storageType": "S3",
|
||||
"storageIds": {},
|
||||
"doNotCache": true
|
||||
};
|
||||
}
|
||||
|
||||
Response getStorageSetting(Context context) {
|
||||
switch(context.pathParams.get("file")){
|
||||
case _engineName:
|
||||
return Response(body: _engineIni);
|
||||
case _gameName:
|
||||
return Response(body: _gameIni);
|
||||
case _runtimeName:
|
||||
return Response(body: _runtimeIni);
|
||||
default:
|
||||
return Response();
|
||||
}
|
||||
}
|
||||
|
||||
Response getStorageFile(Context context) {
|
||||
if (context.pathParams.get("file")?.toLowerCase() != "clientsettings.sav") {
|
||||
return Response.json(
|
||||
{"error": "File not found"},
|
||||
statusCode: 404
|
||||
);
|
||||
}
|
||||
|
||||
var file = _getSettingsFile(context);
|
||||
return Response(
|
||||
body: file.existsSync() ? file.readAsBytesSync() : null,
|
||||
headers: {"content-type": "application/octet-stream"}
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> getStorageAccount(Context context) {
|
||||
var file = _getSettingsFile(context);
|
||||
if (!file.existsSync()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var content = file.readAsBytesSync();
|
||||
return [{
|
||||
"uniqueFilename": "ClientSettings.Sav",
|
||||
"filename": "ClientSettings.Sav",
|
||||
"hash": sha1.convert(content).toString(),
|
||||
"hash256": sha256.convert(content).toString(),
|
||||
"length": content.length,
|
||||
"contentType": "application/octet-stream",
|
||||
"uploaded": "2020-02-23T18:35:53.967Z",
|
||||
"storageType": "S3",
|
||||
"storageIds": {},
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"doNotCache": true
|
||||
}];
|
||||
}
|
||||
|
||||
Future<Response> addStorageFile(Context context) async {
|
||||
if(!_settings.existsSync()){
|
||||
await _settings.create(recursive: true);
|
||||
}
|
||||
|
||||
var file = _getSettingsFile(context);
|
||||
await file.writeAsBytes(await context.body);
|
||||
return Response(statusCode: 204);
|
||||
}
|
||||
|
||||
File _getSettingsFile(Context context) {
|
||||
if(!_settings.existsSync()){
|
||||
_settings.createSync(recursive: true);
|
||||
}
|
||||
|
||||
return File("${_settings.path}\\ClientSettings-${parseSeasonBuild(context)}.Sav");
|
||||
}
|
||||
19
lib/src/embedded/storefront.dart
Normal file
19
lib/src/embedded/storefront.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:jaguar/http/response/response.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
|
||||
final String _keyChain = loadEmbedded("responses/keychain.json").readAsStringSync();
|
||||
final String _catalog = loadEmbedded("responses/catalog.json").readAsStringSync();
|
||||
|
||||
Response getCatalog(Context context) {
|
||||
if (context.headers.value("user-agent")?.contains("2870186") == true) {
|
||||
return Response(statusCode: 404);
|
||||
}
|
||||
|
||||
return Response(body: _catalog, headers: {"content-type": "application/json"});
|
||||
}
|
||||
|
||||
Response getKeyChain(Context context) => Response(body: _keyChain, headers: {"content-type": "application/json"});
|
||||
|
||||
Map<String, Object> getOffers(Context context) => {};
|
||||
42
lib/src/embedded/utils.dart
Normal file
42
lib/src/embedded/utils.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
const String _chars =
|
||||
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
||||
final Random _random = Random();
|
||||
|
||||
String randomString(int length) => String.fromCharCodes(
|
||||
Iterable.generate(length, (_) => _chars.codeUnitAt(_random.nextInt(_chars.length))));
|
||||
|
||||
double parseSeasonBuild(Context context){
|
||||
String? userAgent = context.headers.value("user-agent");
|
||||
if (userAgent == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
try {
|
||||
var build = userAgent.split("Release-")[1].split("-")[0];
|
||||
if (build.split(".").length == 3) {
|
||||
var value = build.split(".");
|
||||
return double.parse("${value[0]}.${value[1]}${value[2]}");
|
||||
}
|
||||
|
||||
return double.parse(build);
|
||||
} catch (_) {
|
||||
return 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
int parseSeason(Context context) => int.parse(parseSeasonBuild(context).toString().split(".")[0]);
|
||||
|
||||
Future<HashMap<String, String?>> parseBody(Context context) async {
|
||||
var params = HashMap<String, String?>();
|
||||
utf8.decode(await context.req.body)
|
||||
.split("&")
|
||||
.map((entry) => MapEntry(entry.substring(0, entry.indexOf("=")), entry.substring(entry.indexOf("=") + 1)))
|
||||
.forEach((element) => params[element.key] = Uri.decodeQueryComponent(element.value));
|
||||
return params;
|
||||
}
|
||||
41
lib/src/embedded/version.dart
Normal file
41
lib/src/embedded/version.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/util/time.dart';
|
||||
|
||||
Map<String, Object> getVersion(Context context) => {
|
||||
"app": "fortnite",
|
||||
"serverDate": "2022-11-08T18:55:52.341Z",
|
||||
"overridePropertiesVersion": "unknown",
|
||||
"cln": "17951730",
|
||||
"build": "444",
|
||||
"moduleName": "Fortnite-Core",
|
||||
"buildDate": "2021-10-27T21:00:51.697Z",
|
||||
"version": "18.30",
|
||||
"branch": "Release-18.30",
|
||||
"modules": {
|
||||
"Epic-LightSwitch-AccessControlCore": {
|
||||
"cln": "17237679",
|
||||
"build": "b2130",
|
||||
"buildDate": "2021-08-19T18:56:08.144Z",
|
||||
"version": "1.0.0",
|
||||
"branch": "trunk"
|
||||
},
|
||||
"epic-xmpp-api-v1-base": {
|
||||
"cln": "5131a23c1470acbd9c94fae695ef7d899c1a41d6",
|
||||
"build": "b3595",
|
||||
"buildDate": "2019-07-30T09:11:06.587Z",
|
||||
"version": "0.0.1",
|
||||
"branch": "master"
|
||||
},
|
||||
"epic-common-core": {
|
||||
"cln": "17909521",
|
||||
"build": "3217",
|
||||
"buildDate": "2021-10-25T18:41:12.486Z",
|
||||
"version": "3.0",
|
||||
"branch": "TRUNK"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Map<String, Object> hasUpdate(Context context) => {
|
||||
"type": "NO_UPDATE"
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import 'package:version/version.dart';
|
||||
class FortniteBuild {
|
||||
final Version version;
|
||||
final String link;
|
||||
final bool hasManifest;
|
||||
|
||||
FortniteBuild({required this.version, required this.link, required this.hasManifest});
|
||||
FortniteBuild({required this.version, required this.link});
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ import 'package:path/path.dart' as path;
|
||||
class FortniteVersion {
|
||||
String name;
|
||||
Directory location;
|
||||
bool memoryFix;
|
||||
|
||||
FortniteVersion.fromJson(json)
|
||||
: name = json["name"],
|
||||
location = Directory(json["location"]),
|
||||
memoryFix = json["memory_fix"] ?? false;
|
||||
location = Directory(json["location"]);
|
||||
|
||||
FortniteVersion({required this.name, required this.location, required this.memoryFix});
|
||||
FortniteVersion({required this.name, required this.location});
|
||||
|
||||
static File? findExecutable(Directory directory, String name) {
|
||||
try{
|
||||
@@ -32,13 +30,17 @@ class FortniteVersion {
|
||||
return findExecutable(location, "FortniteLauncher.exe");
|
||||
}
|
||||
|
||||
File? get eacExecutable {
|
||||
return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'location': location.path,
|
||||
};
|
||||
'name': name,
|
||||
'location': location.path
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FortniteVersion{name: $name, location: $location}';
|
||||
return 'FortniteVersion{name: $name, location: $location';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/page/settings_page.dart';
|
||||
import 'package:reboot_launcher/src/page/launcher_page.dart';
|
||||
import 'package:reboot_launcher/src/page/server_page.dart';
|
||||
@@ -9,6 +17,7 @@ import 'package:reboot_launcher/src/widget/os/window_border.dart';
|
||||
import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import 'info_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -20,117 +29,294 @@ class HomePage extends StatefulWidget {
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener {
|
||||
static const double _headerSize = 48.0;
|
||||
static const double _sectionSize = 94.0;
|
||||
static const double _sectionSize = 100.0;
|
||||
static const double _defaultPadding = 12.0;
|
||||
static const double _openMenuSize = 320.0;
|
||||
static const int _headerButtonCount = 3;
|
||||
static const int _sectionButtonCount = 4;
|
||||
|
||||
bool _focused = true;
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
final GlobalKey _searchKey = GlobalKey();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
|
||||
final RxBool _focused = RxBool(true);
|
||||
final RxInt _index = RxInt(0);
|
||||
|
||||
bool _shouldMaximize = false;
|
||||
int _index = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
_searchController.addListener(() {
|
||||
if (searchValue.isEmpty) {
|
||||
_searchItems.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_searchItems.value = _allItems.whereType<PaneItem>()
|
||||
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
|
||||
.toList()
|
||||
.cast<NavigationPaneItem>();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
_searchFocusNode.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
setState(() => _focused = true);
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
setState(() => _focused = !_focused);
|
||||
_focused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
void onWindowMoved() {
|
||||
_settingsController.saveWindowOffset(appWindow.position);
|
||||
super.onWindowMoved();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
if(!_gameController.started() || !_serverController.started()) {
|
||||
windowManager.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return InfoDialog(
|
||||
text: "Closing the launcher while a backend is running may make the game not work correctly. Are you sure you want to proceed?",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: "Don't close",
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
onTap: () => windowManager.destroy(),
|
||||
text: "Close",
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) => _calculateSize(),
|
||||
child: SizeChangedLayoutNotifier(
|
||||
child: Obx(() => Stack(
|
||||
children: [
|
||||
_createNavigationView(),
|
||||
_createTitleBar(),
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
_createTopDisplayGestures(),
|
||||
if(_focused() && isWin11)
|
||||
const WindowBorder()
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
Padding _createTopDisplayGestures() => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: _sectionSize * _sectionButtonCount,
|
||||
right: _headerSize * _headerButtonCount,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: _headerSize,
|
||||
child: _createWindowGestures()
|
||||
)
|
||||
);
|
||||
|
||||
GestureDetector _createWindowGestures({Widget? child}) => GestureDetector(
|
||||
onDoubleTap: () {
|
||||
if(!_shouldMaximize){
|
||||
return;
|
||||
}
|
||||
|
||||
appWindow.maximizeOrRestore();
|
||||
_shouldMaximize = false;
|
||||
},
|
||||
onDoubleTapDown: (details) => _shouldMaximize = true,
|
||||
onHorizontalDragStart: (event) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (event) => appWindow.startDragging(),
|
||||
child: child
|
||||
);
|
||||
|
||||
NavigationView _createNavigationView() => NavigationView(
|
||||
paneBodyBuilder: (body) => _createPage(body),
|
||||
pane: NavigationPane(
|
||||
size: const NavigationPaneSize(
|
||||
topHeight: _headerSize
|
||||
),
|
||||
selected: _selectedIndex,
|
||||
onChanged: (index) {
|
||||
_settingsController.player?.pause();
|
||||
_index.value = index;
|
||||
},
|
||||
displayMode: _settingsController.displayType(),
|
||||
indicator: const EndNavigationIndicator(),
|
||||
items: _createItems(),
|
||||
footerItems: _createFooterItems(),
|
||||
header: _settingsController.displayType() != PaneDisplayMode.open ? null : const SizedBox(height: _defaultPadding),
|
||||
autoSuggestBox: _settingsController.displayType() == PaneDisplayMode.top ? null : TextBox(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
placeholder: 'Search',
|
||||
focusNode: _searchFocusNode
|
||||
),
|
||||
autoSuggestBoxReplacement: _settingsController.displayType() == PaneDisplayMode.top ? null : const Icon(FluentIcons.search),
|
||||
),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: _settingsController.displayType() == PaneDisplayMode.top ? null : (child, animation) => child
|
||||
);
|
||||
|
||||
RenderObjectWidget _createPage(Widget? body) => Padding(
|
||||
padding: _createPagePadding(),
|
||||
child: body
|
||||
);
|
||||
|
||||
EdgeInsets _createPagePadding() {
|
||||
if (_settingsController.displayType() == PaneDisplayMode.top) {
|
||||
return const EdgeInsets.all(_defaultPadding);
|
||||
}
|
||||
|
||||
return const EdgeInsets.only(
|
||||
top: 32,
|
||||
left: _defaultPadding,
|
||||
right: _defaultPadding,
|
||||
bottom: _defaultPadding
|
||||
);
|
||||
}
|
||||
|
||||
int? get _selectedIndex {
|
||||
var searchItems = _searchItems();
|
||||
if (searchItems == null) {
|
||||
return _index();
|
||||
}
|
||||
|
||||
if(_index() >= _allItems.length){
|
||||
return null;
|
||||
}
|
||||
|
||||
var indexOnScreen = searchItems.indexOf(_allItems[_index()]);
|
||||
if (indexOnScreen.isNegative) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return indexOnScreen;
|
||||
}
|
||||
|
||||
List<NavigationPaneItem> get _allItems => [..._createItems(), ..._createFooterItems()];
|
||||
|
||||
List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [
|
||||
if(_settingsController.displayType() != PaneDisplayMode.top)
|
||||
PaneItem(
|
||||
title: const Text("Tutorial"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage()
|
||||
)
|
||||
];
|
||||
|
||||
List<NavigationPaneItem> _createItems() => _searchItems() ?? [
|
||||
PaneItem(
|
||||
title: const Text("Home"),
|
||||
icon: const Icon(FluentIcons.game),
|
||||
body: const LauncherPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Backend"),
|
||||
icon: const Icon(FluentIcons.server_enviroment),
|
||||
body: ServerPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Settings"),
|
||||
icon: const Icon(FluentIcons.settings),
|
||||
body: SettingsPage()
|
||||
),
|
||||
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
PaneItem(
|
||||
title: const Text("Tutorial"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage()
|
||||
)
|
||||
];
|
||||
|
||||
bool _calculateSize() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_settingsController.saveWindowSize();
|
||||
var width = window.physicalSize.width;
|
||||
PaneDisplayMode? newType;
|
||||
if (width <= 1000) {
|
||||
newType = PaneDisplayMode.top;
|
||||
} else if (width >= 1500) {
|
||||
newType = PaneDisplayMode.open;
|
||||
} else if (width > 1000) {
|
||||
newType = PaneDisplayMode.compact;
|
||||
}
|
||||
|
||||
if(newType == null || newType == _settingsController.displayType()){
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsController.displayType.value = newType;
|
||||
_searchItems.value = null;
|
||||
_searchController.text = "";
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget _createTitleBar() => Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: _createTitleBarContent(),
|
||||
);
|
||||
|
||||
Widget _createTitleBarContent() {
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top) {
|
||||
return WindowTitleBar(focused: _focused());
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
NavigationView(
|
||||
pane: NavigationPane(
|
||||
size: const NavigationPaneSize(
|
||||
topHeight: _headerSize
|
||||
),
|
||||
selected: _index,
|
||||
onChanged: (index) => setState(() => _index = index),
|
||||
displayMode: PaneDisplayMode.top,
|
||||
indicator: const EndNavigationIndicator(),
|
||||
items: [
|
||||
PaneItem(
|
||||
title: const Text("Home"),
|
||||
icon: const Icon(FluentIcons.game),
|
||||
body: const LauncherPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Lawin"),
|
||||
icon: const Icon(FluentIcons.server_enviroment),
|
||||
body: ServerPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Settings"),
|
||||
icon: const Icon(FluentIcons.settings),
|
||||
body: SettingsPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Info"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage()
|
||||
)
|
||||
]
|
||||
),
|
||||
SizedBox(
|
||||
width: _settingsController.displayType() == PaneDisplayMode.open ? _openMenuSize : _headerSize,
|
||||
height: _headerSize
|
||||
),
|
||||
|
||||
_createTitleBar(),
|
||||
|
||||
_createGestureHandler(),
|
||||
|
||||
if(_focused && isWin11)
|
||||
const WindowBorder()
|
||||
Expanded(
|
||||
child: _createWindowGestures(
|
||||
child: Container(
|
||||
height: _headerSize,
|
||||
color: Colors.transparent
|
||||
)
|
||||
)
|
||||
),
|
||||
WindowTitleBar(focused: _focused())
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Align _createTitleBar() {
|
||||
return Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: WindowTitleBar(focused: _focused),
|
||||
);
|
||||
}
|
||||
|
||||
// Hacky way to get it to work while having maximum performance and no modifications to external libs
|
||||
Padding _createGestureHandler() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: _sectionSize * _sectionButtonCount,
|
||||
right: _headerSize * _headerButtonCount,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: _headerSize,
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () {
|
||||
if(!_shouldMaximize){
|
||||
return;
|
||||
}
|
||||
|
||||
appWindow.maximizeOrRestore();
|
||||
_shouldMaximize = false;
|
||||
},
|
||||
onDoubleTapDown: (details) => _shouldMaximize = true,
|
||||
onHorizontalDragStart: (event) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (event) => appWindow.startDragging()
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
String get searchValue => _searchController.text;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,50 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:dart_vlc/dart_vlc.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide Card;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class InfoPage extends StatelessWidget {
|
||||
import '../controller/settings_controller.dart';
|
||||
|
||||
class InfoPage extends StatefulWidget {
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
_createVersionInfo(),
|
||||
State<InfoPage> createState() => _InfoPageState();
|
||||
}
|
||||
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
_createAutiesAvatar(),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
const Text("Made by Auties00"),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
_createDiscordButton()
|
||||
],
|
||||
),
|
||||
],
|
||||
class _InfoPageState extends State<InfoPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if(_settingsController.player == null){
|
||||
var player = Player(id: 1);
|
||||
player.open(
|
||||
Media.network("https://cdn.discordapp.com/attachments/1006260074416701450/1038844107986055190/tutorial.mp4")
|
||||
);
|
||||
_settingsController.player = player;
|
||||
}
|
||||
|
||||
_settingsController.player?.play();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Card(
|
||||
child: Video(
|
||||
player: _settingsController.player,
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
scale: 1.0,
|
||||
showControls: true,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Button _createDiscordButton() {
|
||||
return Button(
|
||||
child: const Text("Open file directory"),
|
||||
onPressed: () => launchUrl(Directory(safeBinariesDirectory).uri));
|
||||
}
|
||||
|
||||
CircleAvatar _createAutiesAvatar() {
|
||||
return const CircleAvatar(
|
||||
radius: 48,
|
||||
backgroundImage: AssetImage("assets/images/auties.png"));
|
||||
}
|
||||
|
||||
Align _createVersionInfo() {
|
||||
return const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Text("Version 4.4${kDebugMode ? '-DEBUG' : ''}")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -6,13 +5,12 @@ import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/username_box.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import '../util/reboot.dart';
|
||||
|
||||
class LauncherPage extends StatefulWidget {
|
||||
@@ -27,7 +25,6 @@ class LauncherPage extends StatefulWidget {
|
||||
class _LauncherPageState extends State<LauncherPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -43,12 +40,12 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
|
||||
int? get _updateTime {
|
||||
var storage = GetStorage("update");
|
||||
return storage.read("last_update");
|
||||
return storage.read("last_update_v2");
|
||||
}
|
||||
|
||||
set _updateTime(int? updateTime) {
|
||||
var storage = GetStorage("update");
|
||||
storage.write("last_update", updateTime);
|
||||
storage.write("last_update_v2", updateTime);
|
||||
}
|
||||
|
||||
void _onCancelWarning() {
|
||||
@@ -65,50 +62,62 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: FutureBuilder(
|
||||
future: _gameController.updater ?? Future.value(true),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData && !snapshot.hasError) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16.0),
|
||||
Text("Updating Reboot DLL...")
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return FutureBuilder(
|
||||
future: _gameController.updater ?? Future.value(true),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData && !snapshot.hasError) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if(snapshot.hasError)
|
||||
_createUpdateError(snapshot),
|
||||
UsernameBox(),
|
||||
const VersionSelector(),
|
||||
GameTypeSelector(),
|
||||
const LaunchButton()
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16.0),
|
||||
Text("Updating Reboot DLL...")
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(snapshot.hasError)
|
||||
_createUpdateError(snapshot),
|
||||
UsernameBox(),
|
||||
const VersionSelector(),
|
||||
GameTypeSelector(),
|
||||
const LaunchButton()
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
|
||||
return const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("Cannot update dll"),
|
||||
severity: InfoBarSeverity.warning
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ErrorDialog(
|
||||
exception: snapshot.error!,
|
||||
stackTrace: snapshot.stackTrace!,
|
||||
errorMessageBuilder: (exception) => "Cannot update Reboot dll: ${snapshot.error}"
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("Cannot update dll"),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,27 +13,24 @@ class ServerPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Obx(() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(_serverController.warning.value)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: const Text("The lawin server handles authentication and parties, not game hosting"),
|
||||
severity: InfoBarSeverity.info,
|
||||
onClose: () => _serverController.warning.value = false
|
||||
),
|
||||
return Obx(() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(_serverController.warning.value)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: const Text("The backend server handles authentication and parties, not game hosting"),
|
||||
severity: InfoBarSeverity.info,
|
||||
onClose: () => _serverController.warning.value = false
|
||||
),
|
||||
HostInput(),
|
||||
PortInput(),
|
||||
ServerTypeSelector(),
|
||||
const ServerButton()
|
||||
]
|
||||
)),
|
||||
);
|
||||
),
|
||||
HostInput(),
|
||||
PortInput(),
|
||||
ServerTypeSelector(),
|
||||
const ServerButton()
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,60 +19,57 @@ class SettingsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: "The hostname of the server that hosts the multiplayer matches",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Matchmaking Host",
|
||||
placeholder:
|
||||
"Type the hostname of the server that hosts the multiplayer matches",
|
||||
controller: _settingsController.matchmakingIp,
|
||||
validatorMode: AutovalidateMode.always,
|
||||
validator: checkMatchmaking,
|
||||
enabled: _serverController.type() == ServerType.embedded
|
||||
))
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a server is launched",
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: "The hostname of the server that hosts the multiplayer matches",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Matchmaking Host",
|
||||
placeholder:
|
||||
"Type the hostname of the server that hosts the multiplayer matches",
|
||||
controller: _settingsController.matchmakingIp,
|
||||
validatorMode: AutovalidateMode.always,
|
||||
validator: checkMatchmaking,
|
||||
enabled: _serverController.type() == ServerType.embedded
|
||||
))
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a server is launched",
|
||||
child: FileSelector(
|
||||
label: "Reboot DLL",
|
||||
placeholder: "Type the path to the reboot dll",
|
||||
controller: _settingsController.rebootDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a client is launched",
|
||||
child: FileSelector(
|
||||
label: "Console DLL",
|
||||
placeholder: "Type the path to the console dll",
|
||||
controller: _settingsController.consoleDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected to make the game work",
|
||||
child: FileSelector(
|
||||
label: "Reboot DLL",
|
||||
placeholder: "Type the path to the reboot dll",
|
||||
controller: _settingsController.rebootDll,
|
||||
label: "Cranium DLL",
|
||||
placeholder: "Type the path to the dll used for authentication",
|
||||
controller: _settingsController.authDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a client is launched",
|
||||
child: FileSelector(
|
||||
label: "Console DLL",
|
||||
placeholder: "Type the path to the console dll",
|
||||
controller: _settingsController.consoleDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected to make the game work",
|
||||
child: FileSelector(
|
||||
label: "Cranium DLL",
|
||||
placeholder: "Type the path to the cranium dll",
|
||||
controller: _settingsController.craniumDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always))
|
||||
]),
|
||||
);
|
||||
validatorMode: AutovalidateMode.always))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,51 +5,20 @@ import 'package:html/parser.dart' show parse;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
import 'package:reboot_launcher/src/util/time.dart';
|
||||
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
||||
import 'package:version/version.dart';
|
||||
|
||||
import 'os.dart';
|
||||
|
||||
const _userAgent =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36";
|
||||
|
||||
final _cookieRegex = RegExp("cookie=\"(.*?);");
|
||||
final _manifestSourceUrl = Uri.parse(
|
||||
"https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md");
|
||||
final _archiveCookieUrl = Uri.parse("http://allinstaller.xyz/rel");
|
||||
final _archiveSourceUrl = Uri.parse("http://allinstaller.xyz/rel?i=1");
|
||||
|
||||
|
||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
var futures = await Future.wait([_fetchArchives(), _fetchManifests()]);
|
||||
return futures.expand((element) => element)
|
||||
.toList()
|
||||
..sort((first, second) => first.version.compareTo(second.version));
|
||||
}
|
||||
|
||||
Future<List<FortniteBuild>> _fetchArchives() async {
|
||||
var cookieResponse = await http.get(_archiveCookieUrl);
|
||||
var cookie = _cookieRegex.firstMatch(cookieResponse.body)?.group(1)?.trim();
|
||||
var response =
|
||||
await http.get(_archiveSourceUrl, headers: {"Cookie": cookie!});
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
}
|
||||
|
||||
var document = parse(response.body);
|
||||
var results = <FortniteBuild>[];
|
||||
for (var build in document.querySelectorAll("a[href^='https']")) {
|
||||
var version = parser.tryParse(build.text.replaceAll("Build ", ""));
|
||||
if (version == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.add(FortniteBuild(
|
||||
version: version, link: build.attributes["href"]!, hasManifest: false));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<List<FortniteBuild>> _fetchManifests() async {
|
||||
var response = await http.get(_manifestSourceUrl);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
@@ -68,13 +37,13 @@ Future<List<FortniteBuild>> _fetchManifests() async {
|
||||
var name = children[0].text;
|
||||
var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-"));
|
||||
var version = parser
|
||||
.tryParse(minifiedName.replaceFirst("-CL", ""));
|
||||
.tryParse(minifiedName.replaceFirst("", ""));
|
||||
if (version == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var link = children[2].firstChild!.attributes["href"]!;
|
||||
results.add(FortniteBuild(version: version, link: link, hasManifest: true));
|
||||
results.add(FortniteBuild(version: version, link: link));
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -93,9 +62,12 @@ Future<Process> downloadManifestBuild(
|
||||
}
|
||||
|
||||
Future<void> downloadArchiveBuild(String archiveUrl, String destination,
|
||||
Function(double) onProgress, Function() onRar) async {
|
||||
Function(double, String) onProgress, Function() onDecompress) async {
|
||||
var uuid = Random.secure().nextInt(1000000);
|
||||
var extension = archiveUrl.substring(archiveUrl.lastIndexOf("."));
|
||||
var tempFile = File(
|
||||
"$destination\\.temp\\FortniteBuild${Random.secure().nextInt(1000000)}.rar");
|
||||
"$destination\\.temp\\FortniteBuild$uuid$extension"
|
||||
);
|
||||
await tempFile.parent.create(recursive: true);
|
||||
try {
|
||||
var client = http.Client();
|
||||
@@ -106,27 +78,38 @@ Future<void> downloadArchiveBuild(String archiveUrl, String destination,
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
}
|
||||
|
||||
var startTime = DateTime.now();
|
||||
var lastRemaining = -1;
|
||||
var length = response.contentLength!;
|
||||
var received = 0;
|
||||
var sink = tempFile.openWrite();
|
||||
await response.stream.map((s) {
|
||||
received += s.length;
|
||||
onProgress((received / length) * 100);
|
||||
return s;
|
||||
var lastEta = toETA(0);
|
||||
await response.stream.map((entry) {
|
||||
received += entry.length;
|
||||
var percentage = (received / length) * 100;
|
||||
var elapsed = DateTime.now().difference(startTime).inMilliseconds;
|
||||
var newRemaining = (elapsed * length / received - elapsed).round();
|
||||
if(lastRemaining < 0 || lastRemaining - newRemaining <= -10000 || lastRemaining > newRemaining) {
|
||||
lastEta = toETA(lastRemaining = newRemaining);
|
||||
}
|
||||
|
||||
onProgress(percentage, lastEta);
|
||||
return entry;
|
||||
}).pipe(sink);
|
||||
onRar();
|
||||
onDecompress();
|
||||
|
||||
var output = Directory(destination);
|
||||
await output.create(recursive: true);
|
||||
await loadBinary("winrar.exe", true);
|
||||
var shell = Shell(
|
||||
commandVerbose: false,
|
||||
commentVerbose: false,
|
||||
workingDirectory: internalBinariesDirectory
|
||||
workingDirectory: safeBinariesDirectory
|
||||
);
|
||||
await shell.run("./winrar.exe x ${tempFile.path} *.* \"${output.path}\"");
|
||||
await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\"");
|
||||
} finally {
|
||||
if (await tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
if (await tempFile.parent.exists()) {
|
||||
tempFile.parent.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
lib/src/util/error.dart
Normal file
24
lib/src/util/error.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../dialog/dialog.dart';
|
||||
|
||||
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||
if(exception == null){
|
||||
return;
|
||||
}
|
||||
|
||||
if(appKey.currentContext == null || appKey.currentState?.mounted == false){
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: exception,
|
||||
stackTrace: stackTrace,
|
||||
errorMessageBuilder: (exception) => framework ? "An error was thrown by Flutter: $exception" : "An uncaught error was thrown: $exception"
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:win32/win32.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'dart:ffi';
|
||||
|
||||
const int appBarSize = 2;
|
||||
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||
|
||||
@@ -31,6 +35,18 @@ Future<File> loadBinary(String binary, bool safe) async{
|
||||
return safeBinary;
|
||||
}
|
||||
|
||||
Future<bool> runElevated(String executable, String args) async {
|
||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||
shellInput.ref.nShow = SW_SHOWDEFAULT;
|
||||
shellInput.ref.fMask = 0x00000040;
|
||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
||||
var shellResult = ShellExecuteEx(shellInput);
|
||||
return shellResult == 1;
|
||||
}
|
||||
|
||||
File _locateInternalBinary(String binary){
|
||||
return File("$internalBinariesDirectory\\$binary");
|
||||
}
|
||||
@@ -43,3 +59,12 @@ Directory get tempDirectory =>
|
||||
|
||||
String get safeBinariesDirectory =>
|
||||
"${Platform.environment["UserProfile"]}\\.reboot_launcher";
|
||||
|
||||
File loadEmbedded(String file) {
|
||||
var safeBinary = File("$safeBinariesDirectory\\backend\\cli\\$file");
|
||||
if(safeBinary.existsSync()){
|
||||
return safeBinary;
|
||||
}
|
||||
|
||||
return File("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\$file");
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
|
||||
const _rebootUrl =
|
||||
"https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip";
|
||||
"https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip";
|
||||
|
||||
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
||||
return lastUpdateMs != null ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) : null;
|
||||
@@ -29,14 +29,18 @@ Future<int> downloadRebootDll(int? lastUpdateMs) async {
|
||||
var outputDir = await tempDirectory.createTemp("reboot");
|
||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||
|
||||
var rebootDll = outputDir.listSync()
|
||||
.firstWhere((element) => path.extension(element.path) == ".dll");
|
||||
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) {
|
||||
var rebootDll = File(
|
||||
outputDir.listSync()
|
||||
.firstWhere((element) => path.extension(element.path) == ".dll")
|
||||
.path
|
||||
);
|
||||
|
||||
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await rebootDll.readAsBytes())) {
|
||||
outputDir.delete(recursive: true);
|
||||
return now.millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
await rebootDll.rename(oldRebootDll.path);
|
||||
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
|
||||
outputDir.delete(recursive: true);
|
||||
return now.millisecondsSinceEpoch;
|
||||
}
|
||||
17
lib/src/util/selector.dart
Normal file
17
lib/src/util/selector.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
Future<String?> openFolderPicker(String title) async =>
|
||||
await FilePicker.platform.getDirectoryPath(dialogTitle: title);
|
||||
|
||||
Future<String?> openFilePicker(String extension) async {
|
||||
var result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowMultiple: false,
|
||||
allowedExtensions: [extension]
|
||||
);
|
||||
if(result == null || result.files.isEmpty){
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.files.first.path;
|
||||
}
|
||||
@@ -1,29 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
|
||||
final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Lawin.exe");
|
||||
final serverConfig = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Config\\config.ini");
|
||||
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
|
||||
|
||||
const String _serverUrl =
|
||||
"https://cdn.discordapp.com/attachments/1031262639457828910/1034506676843327549/lawin.zip";
|
||||
|
||||
Future<bool> downloadServer(ignored) async {
|
||||
var response = await http.get(Uri.parse(_serverUrl));
|
||||
var tempZip = File("${Platform.environment["Temp"]}/lawin.zip");
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
await extractFileToDisk(tempZip.path, serverLocation.parent.path);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> isLawinPortFree() async {
|
||||
try {
|
||||
var portBat = await loadBinary("port.bat", true);
|
||||
@@ -39,27 +25,41 @@ Future<bool> isLawinPortFree() async {
|
||||
|
||||
Future<void> freeLawinPort() async {
|
||||
var releaseBat = await loadBinary("release.bat", false);
|
||||
await Process.run(releaseBat.path, []);
|
||||
var result = await Process.run(releaseBat.path, []);
|
||||
if(!result.outText.contains("Access is denied")){
|
||||
return;
|
||||
}
|
||||
|
||||
await runElevated(releaseBat.path, "");
|
||||
}
|
||||
|
||||
List<String> createRebootArgs(String username, bool headless) {
|
||||
List<String> createRebootArgs(String username, GameType type) {
|
||||
var args = [
|
||||
"-skippatchcheck",
|
||||
"-epicapp=Fortnite",
|
||||
"-epicenv=Prod",
|
||||
"-epiclocale=en-us",
|
||||
"-epicportal",
|
||||
"-noeac",
|
||||
"-fromfl=be",
|
||||
"-fltoken=7ce411021b27b4343a44fdg8",
|
||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
||||
"-AUTH_LOGIN=$username@projectreboot.dev",
|
||||
"-AUTH_PASSWORD=Rebooted",
|
||||
"-AUTH_TYPE=epic"
|
||||
"-skippatchcheck",
|
||||
"-nobe",
|
||||
"-fromfl=eac",
|
||||
"-fltoken=3db3ba5dcbd2e16703f3978d",
|
||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ"
|
||||
];
|
||||
|
||||
if(headless){
|
||||
args.addAll(["-nullrhi", "-nosplash", "-nosound"]);
|
||||
if(username.isNotEmpty){
|
||||
args.addAll([
|
||||
"-AUTH_LOGIN=${username.replaceAll(RegExp("[^A-Za-z0-9]"), "")}@projectreboot.dev",
|
||||
"-AUTH_PASSWORD=Rebooted",
|
||||
"-AUTH_TYPE=epic"
|
||||
]);
|
||||
}
|
||||
|
||||
if(type == GameType.headlessServer){
|
||||
args.addAll([
|
||||
"-nullrhi",
|
||||
"-nosplash",
|
||||
"-nosound",
|
||||
]);
|
||||
}
|
||||
|
||||
return args;
|
||||
@@ -118,8 +118,7 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
||||
if (!free) {
|
||||
if(!needsFreePort) {
|
||||
return ServerResult(
|
||||
uri: pingSelf(port),
|
||||
type: ServerResultType.ignoreStart
|
||||
type: ServerResultType.alreadyStarted
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,54 +128,22 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
||||
}
|
||||
}
|
||||
|
||||
if(type == ServerType.embedded && !serverLocation.existsSync()){
|
||||
return ServerResult(
|
||||
type: ServerResultType.serverDownloadRequiredError
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
uri: ping(host, port),
|
||||
type: ServerResultType.canStart
|
||||
);
|
||||
}
|
||||
|
||||
Future<Process?> startEmbeddedServer() async {
|
||||
await resetServerLog();
|
||||
try {
|
||||
var process = await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path);
|
||||
process.outLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append));
|
||||
process.errLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append));
|
||||
return process;
|
||||
} on ProcessException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<HttpServer> startRemoteServer(Uri uri) async {
|
||||
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
}
|
||||
|
||||
Future<void> resetServerLog() async {
|
||||
try {
|
||||
if(await serverLogFile.exists()) {
|
||||
await serverLogFile.delete();
|
||||
}
|
||||
|
||||
await serverLogFile.create();
|
||||
}catch(_){
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
class ServerResult {
|
||||
final Future<Uri?>? uri;
|
||||
final int? pid;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
final ServerResultType type;
|
||||
|
||||
ServerResult({this.uri, this.pid, this.error, this.stackTrace, required this.type});
|
||||
ServerResult({this.pid, this.error, this.stackTrace, required this.type});
|
||||
}
|
||||
|
||||
enum ServerResultType {
|
||||
@@ -185,10 +152,8 @@ enum ServerResultType {
|
||||
illegalPortError,
|
||||
cannotPingServer,
|
||||
portTakenError,
|
||||
serverDownloadRequiredError,
|
||||
canStart,
|
||||
ignoreStart,
|
||||
started,
|
||||
alreadyStarted,
|
||||
unknownError,
|
||||
stopped,
|
||||
}
|
||||
47
lib/src/util/time.dart
Normal file
47
lib/src/util/time.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
String toETA(int milliseconds){
|
||||
var duration = Duration(milliseconds: milliseconds);
|
||||
return "${duration.inHours.toString().padLeft(2, "0")}:"
|
||||
"${duration.inMinutes.remainder(60).toString().padLeft(2, "0")}:"
|
||||
"${duration.inSeconds.remainder(60).toString().padLeft(2, "0")}";
|
||||
}
|
||||
|
||||
extension DateTimeIso on DateTime {
|
||||
String toIsoString() {
|
||||
String y = (year >= -9999 && year <= 9999) ? _fourDigits(year) : _sixDigits(year);
|
||||
String m = _twoDigits(month);
|
||||
String d = _twoDigits(day);
|
||||
String h = _twoDigits(hour);
|
||||
String min = _twoDigits(minute);
|
||||
String sec = _twoDigits(second);
|
||||
String ms = _threeDigits(millisecond);
|
||||
return "$y-$m-${d}T$h:$min:$sec.${ms}Z";
|
||||
}
|
||||
|
||||
static String _fourDigits(int n) {
|
||||
int absN = n.abs();
|
||||
String sign = n < 0 ? "-" : "";
|
||||
if (absN >= 1000) return "$n";
|
||||
if (absN >= 100) return "${sign}0$absN";
|
||||
if (absN >= 10) return "${sign}00$absN";
|
||||
return "${sign}000$absN";
|
||||
}
|
||||
|
||||
static String _sixDigits(int n) {
|
||||
assert(n < -9999 || n > 9999);
|
||||
int absN = n.abs();
|
||||
String sign = n < 0 ? "-" : "+";
|
||||
if (absN >= 100000) return "$sign$absN";
|
||||
return "${sign}0$absN";
|
||||
}
|
||||
|
||||
static String _threeDigits(int n) {
|
||||
if (n >= 100) return "$n";
|
||||
if (n >= 10) return "0$n";
|
||||
return "00$n";
|
||||
}
|
||||
|
||||
static String _twoDigits(int n) {
|
||||
if (n >= 10) return "$n";
|
||||
return "0$n";
|
||||
}
|
||||
}
|
||||
@@ -37,9 +37,8 @@ class _BuildSelectorState extends State<BuildSelector> {
|
||||
|
||||
ComboBoxItem<FortniteBuild> _createItem(FortniteBuild element) {
|
||||
return ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(
|
||||
"${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"),
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -9,10 +9,10 @@ import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/game_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/injector.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||
@@ -21,6 +21,7 @@ import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../controller/settings_controller.dart';
|
||||
import '../../dialog/snackbar.dart';
|
||||
|
||||
@@ -34,6 +35,15 @@ class LaunchButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LaunchButtonState extends State<LaunchButton> {
|
||||
final List<String> _errorStrings = [
|
||||
"port 3551 failed: Connection refused",
|
||||
"Unable to login to Fortnite servers",
|
||||
"HTTP 400 response from ",
|
||||
"Network failure when attempting to check platform restrictions",
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
@@ -70,13 +80,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.started.value = true;
|
||||
if (_gameController.username.text.isEmpty) {
|
||||
showMessage("Missing in-game username");
|
||||
if (_gameController.username.text.isEmpty && _gameController.type() != GameType.client) {
|
||||
showMessage("Missing username");
|
||||
_gameController.started.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.started.value = true;
|
||||
if (_gameController.selectedVersionObs.value == null) {
|
||||
showMessage("No version is selected");
|
||||
_gameController.started.value = false;
|
||||
@@ -87,7 +97,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
var version = _gameController.selectedVersionObs.value!;
|
||||
var gamePath = version.executable?.path;
|
||||
if(gamePath == null){
|
||||
_onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null);
|
||||
_onError("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?", null);
|
||||
_onStop();
|
||||
return;
|
||||
}
|
||||
@@ -97,8 +107,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
Win32Process(_gameController.launcherProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
var result = await _serverController.changeStateInteractive(true);
|
||||
if (version.eacExecutable != null) {
|
||||
_gameController.eacProcess = await Process.start(version.eacExecutable!.path, []);
|
||||
Win32Process(_gameController.eacProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
var result = await _serverController.start(
|
||||
required: true,
|
||||
askPortKill: false,
|
||||
);
|
||||
if(!result){
|
||||
showMessage("Cannot launch the game as the backend didn't start up correctly");
|
||||
_onStop();
|
||||
return;
|
||||
}
|
||||
@@ -107,15 +126,16 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
await _logFile!.delete();
|
||||
}
|
||||
|
||||
|
||||
await patch(version.executable!);
|
||||
await compute(patchMatchmaking, version.executable!);
|
||||
await compute(patchHeadless, version.executable!);
|
||||
|
||||
var headlessHosting = _gameController.type() == GameType.headlessServer;
|
||||
var arguments = createRebootArgs(_gameController.username.text, headlessHosting);
|
||||
var arguments = createRebootArgs(_gameController.username.text, _gameController.type.value);
|
||||
_gameController.gameProcess = await Process.start(gamePath, arguments)
|
||||
..exitCode.then((_) => _onEnd())
|
||||
..outLines.forEach((line) => _onGameOutput(line, version.memoryFix))
|
||||
..errLines.forEach((line) => _onGameOutput(line, version.memoryFix));
|
||||
..outLines.forEach((line) => _onGameOutput(line))
|
||||
..errLines.forEach((line) => _onGameOutput(line));
|
||||
_injectOrShowError(Injectable.cranium);
|
||||
if(headlessHosting){
|
||||
await _showServerLaunchingWarning();
|
||||
}
|
||||
@@ -126,17 +146,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> patch(File file) async {
|
||||
switch(_gameController.type()){
|
||||
case GameType.client:
|
||||
return await compute(patchMatchmaking, file);
|
||||
case GameType.server:
|
||||
return false;
|
||||
case GameType.headlessServer:
|
||||
return await compute(patchHeadless, file);
|
||||
}
|
||||
}
|
||||
|
||||
void _onEnd() {
|
||||
if(_fail){
|
||||
return;
|
||||
@@ -147,25 +156,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
void _closeDialogIfOpen(bool success) {
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var route = ModalRoute.of(context);
|
||||
var route = ModalRoute.of(appKey.currentContext!);
|
||||
if(route == null || route.isCurrent){
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(success);
|
||||
Navigator.of(appKey.currentContext!).pop(success);
|
||||
}
|
||||
|
||||
Future<void> _showServerLaunchingWarning() async {
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await showDialog<bool>(
|
||||
context: context,
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => ProgressDialog(
|
||||
text: "Launching headless server...",
|
||||
onStop: () {
|
||||
@@ -182,11 +183,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_onStop();
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, bool memoryFix) {
|
||||
if(kDebugMode){
|
||||
print(line);
|
||||
}
|
||||
|
||||
void _onGameOutput(String line) {
|
||||
if(_logFile != null){
|
||||
_logFile!.writeAsString("$line\n", mode: FileMode.append);
|
||||
}
|
||||
@@ -196,33 +193,18 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){
|
||||
if(_errorStrings.any((element) => line.contains(element))){
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showBrokenError();
|
||||
_showTokenError();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("HTTP 400 response from ")){
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showUnsupportedHeadless();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showTokenError();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Platform has ")){
|
||||
_injectOrShowError(Injectable.cranium);
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Login: Completing Sign-in")){
|
||||
if(line.contains("Region ")){
|
||||
if(_gameController.type.value == GameType.client){
|
||||
_injectOrShowError(Injectable.console);
|
||||
}else {
|
||||
@@ -230,9 +212,19 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
.then((value) => _closeDialogIfOpen(true));
|
||||
}
|
||||
|
||||
if(memoryFix){
|
||||
_injectOrShowError(Injectable.memoryFix);
|
||||
}
|
||||
_injectOrShowError(Injectable.memoryFix);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showTokenError() async {
|
||||
if(_serverController.type() == ServerType.embedded) {
|
||||
showTokenErrorFixable();
|
||||
await _serverController.start(
|
||||
required: true,
|
||||
askPortKill: false
|
||||
);
|
||||
} else {
|
||||
showTokenErrorUnfixable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +263,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
await injectDll(gameProcess.pid, dllPath.path);
|
||||
} catch (exception) {
|
||||
showSnackbar(
|
||||
context,
|
||||
appKey.currentContext!,
|
||||
Snackbar(
|
||||
content: Text("Cannot inject $injectable.dll: $exception", textAlign: TextAlign.center),
|
||||
extended: true
|
||||
@@ -283,6 +275,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _onDllFail(File dllPath) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showMissingDllError(path.basename(dllPath.path));
|
||||
@@ -297,9 +293,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
case Injectable.console:
|
||||
return File(_settingsController.consoleDll.text);
|
||||
case Injectable.cranium:
|
||||
return File(_settingsController.craniumDll.text);
|
||||
return File(_settingsController.authDll.text);
|
||||
case Injectable.memoryFix:
|
||||
return await loadBinary("fix.dll", true);
|
||||
return await loadBinary("leakv2.dll", true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/dialog/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart';
|
||||
@@ -12,6 +14,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../dialog/add_server_version.dart';
|
||||
import '../../util/checks.dart';
|
||||
import '../shared/file_selector.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
const VersionSelector({Key? key}) : super(key: key);
|
||||
@@ -58,9 +61,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
Widget _createSelector(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The version of Fortnite to launch",
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Obx(() => DropDownButton(
|
||||
child: Obx(() => DropDownButton(
|
||||
leading: Text(_gameController.selectedVersionObs.value?.name ??
|
||||
"Select a version"),
|
||||
items: _gameController.hasNoVersions
|
||||
@@ -68,28 +69,26 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
: _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList()))
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(
|
||||
BuildContext context, FortniteVersion version) {
|
||||
return MenuFlyoutItem(
|
||||
text: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse ||
|
||||
event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
text: Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse ||
|
||||
event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _openMenu(context, version, event.position);
|
||||
},
|
||||
child: Text(version.name)
|
||||
)
|
||||
await _openMenu(context, version, event.position);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(version.name)
|
||||
),
|
||||
),
|
||||
trailing: const Expanded(child: SizedBox()),
|
||||
onPressed: () => _gameController.selectedVersion = version);
|
||||
}
|
||||
|
||||
@@ -120,7 +119,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
context: context,
|
||||
offset: offset,
|
||||
builder: (context) => MenuFlyout(
|
||||
items: ContextualOption.getValues(version.memoryFix)
|
||||
items: ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.toList()
|
||||
)
|
||||
@@ -137,18 +136,13 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
.onError((error, stackTrace) => _onExplorerError());
|
||||
break;
|
||||
|
||||
case ContextualOption.rename:
|
||||
case ContextualOption.modify:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop();
|
||||
var result = await _openRenameDialog(context, version);
|
||||
if(result == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.rename(version, result);
|
||||
await _openRenameDialog(context, version);
|
||||
break;
|
||||
|
||||
case ContextualOption.delete:
|
||||
@@ -173,24 +167,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
break;
|
||||
case ContextualOption.enableMemoryFix:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
version.memoryFix = true;
|
||||
Navigator.of(context).pop();
|
||||
showMessage("Enabled memory fix");
|
||||
break;
|
||||
case ContextualOption.disableMemoryFix:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
version.memoryFix = false;
|
||||
Navigator.of(context).pop();
|
||||
showMessage("Disabled memory fix");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
@@ -251,46 +227,57 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) {
|
||||
var controller = TextEditingController(text: version.name);
|
||||
var nameController = TextEditingController(text: version.name);
|
||||
var pathController = TextEditingController(text: version.location.path);
|
||||
return showDialog<String?>(
|
||||
context: context,
|
||||
builder: (context) => Form(
|
||||
child: Builder(
|
||||
builder: (context) => ContentDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormBox(
|
||||
controller: controller,
|
||||
header: "Name",
|
||||
placeholder: "Type the new version name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||
),
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormBox(
|
||||
controller: nameController,
|
||||
header: "Name",
|
||||
placeholder: "Type the new version name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (!Form.of(context)!.validate()) {
|
||||
return;
|
||||
}
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
Navigator.of(context).pop(controller.text);
|
||||
},
|
||||
child: const Text('Save')
|
||||
)
|
||||
]
|
||||
FileSelector(
|
||||
label: "Location",
|
||||
placeholder: "Type the new game folder",
|
||||
windowTitle: "Select game folder",
|
||||
controller: pathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: "Save",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.updateVersion(version, (version) {
|
||||
version.name = nameController.text;
|
||||
version.location = Directory(pathController.text);
|
||||
});
|
||||
},
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -298,22 +285,12 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
|
||||
enum ContextualOption {
|
||||
openExplorer,
|
||||
rename,
|
||||
enableMemoryFix,
|
||||
disableMemoryFix,
|
||||
modify,
|
||||
delete;
|
||||
|
||||
static List<ContextualOption> getValues(bool memoryFix){
|
||||
return memoryFix
|
||||
? [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.disableMemoryFix, ContextualOption.delete]
|
||||
: [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.enableMemoryFix, ContextualOption.delete];
|
||||
}
|
||||
|
||||
String get name {
|
||||
return this == ContextualOption.openExplorer ? "Open in explorer"
|
||||
: this == ContextualOption.rename ? "Rename"
|
||||
: this == ContextualOption.enableMemoryFix ? "Enable memory leak fix"
|
||||
: this == ContextualOption.disableMemoryFix ? "Disable memory leak fix"
|
||||
: this == ContextualOption.modify ? "Modify"
|
||||
: "Delete";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ class HostInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The hostname of the lawin server",
|
||||
message: "The hostname of the backend server",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Host",
|
||||
placeholder: "Type the lawin server's hostname",
|
||||
placeholder: "Type the backend server's hostname",
|
||||
controller: _serverController.host,
|
||||
enabled: _serverController.type.value == ServerType.remote
|
||||
))
|
||||
|
||||
@@ -13,10 +13,10 @@ class PortInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The port of the lawin server",
|
||||
message: "The port of the backend server",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Port",
|
||||
placeholder: "Type the lawin server's port",
|
||||
placeholder: "Type the backend server's port",
|
||||
controller: _serverController.port,
|
||||
enabled: _serverController.type.value != ServerType.embedded
|
||||
))
|
||||
|
||||
@@ -23,7 +23,10 @@ class _ServerButtonState extends State<ServerButton> {
|
||||
child: Obx(() => Tooltip(
|
||||
message: _helpMessage,
|
||||
child: Button(
|
||||
onPressed: () async => _serverController.changeStateInteractive(false),
|
||||
onPressed: () async => _serverController.start(
|
||||
required: false,
|
||||
askPortKill: true
|
||||
),
|
||||
child: Text(_buttonText())),
|
||||
)),
|
||||
),
|
||||
@@ -46,18 +49,18 @@ class _ServerButtonState extends State<ServerButton> {
|
||||
switch(_serverController.type.value){
|
||||
case ServerType.embedded:
|
||||
if (_serverController.started.value) {
|
||||
return "Stop the lawin server currently running";
|
||||
return "Stop the backend server currently running";
|
||||
}
|
||||
|
||||
return "Start a new local lawin server";
|
||||
return "Start a new local backend server";
|
||||
case ServerType.remote:
|
||||
if (_serverController.started.value) {
|
||||
return "Stop the reverse proxy currently running";
|
||||
}
|
||||
|
||||
return "Start a reverse proxy targeting the remote lawin server";
|
||||
return "Start a reverse proxy targeting the remote backend server";
|
||||
case ServerType.local:
|
||||
return "Check if a local lawin server is running";
|
||||
return "Check if a local backend server is running";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class ServerTypeSelector extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "Determines the type of lawin server to use",
|
||||
message: "Determines the type of backend server to use",
|
||||
child: InfoLabel(
|
||||
label: "Type",
|
||||
child: SizedBox(
|
||||
@@ -35,7 +35,10 @@ class ServerTypeSelector extends StatelessWidget {
|
||||
child: Text(type.name)
|
||||
)
|
||||
),
|
||||
onPressed: () => _serverController.type(type)
|
||||
onPressed: () async {
|
||||
await _serverController.stop();
|
||||
_serverController.type(type);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
106
lib/src/widget/shared/file_selector.dart
Normal file
106
lib/src/widget/shared/file_selector.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
|
||||
import '../../util/selector.dart';
|
||||
|
||||
class FileSelector extends StatefulWidget {
|
||||
final String label;
|
||||
final String placeholder;
|
||||
final String windowTitle;
|
||||
final bool allowNavigator;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?) validator;
|
||||
final AutovalidateMode? validatorMode;
|
||||
final String? extension;
|
||||
final bool folder;
|
||||
|
||||
const FileSelector(
|
||||
{required this.label,
|
||||
required this.placeholder,
|
||||
required this.windowTitle,
|
||||
required this.controller,
|
||||
required this.validator,
|
||||
required this.folder,
|
||||
this.extension,
|
||||
this.validatorMode,
|
||||
this.allowNavigator = true,
|
||||
Key? key})
|
||||
: assert(folder || extension != null, "Missing extension for file selector"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<FileSelector> createState() => _FileSelectorState();
|
||||
}
|
||||
|
||||
class _FileSelectorState extends State<FileSelector> {
|
||||
final RxBool _valid = RxBool(true);
|
||||
late String? Function(String?) validator;
|
||||
bool _selecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
validator = (value) {
|
||||
var result = widget.validator(value);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _valid.value = result == null);
|
||||
return result;
|
||||
};
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfoLabel(
|
||||
label: widget.label,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
controller: widget.controller,
|
||||
placeholder: widget.placeholder,
|
||||
validator: validator,
|
||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
|
||||
)
|
||||
),
|
||||
if (widget.allowNavigator) const SizedBox(width: 8.0),
|
||||
if (widget.allowNavigator)
|
||||
Tooltip(
|
||||
message: "Select a ${widget.folder ? 'folder' : 'file'}",
|
||||
child: Obx(() => Padding(
|
||||
padding: _valid() ? EdgeInsets.zero : const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
))
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _onPressed() {
|
||||
if(_selecting){
|
||||
showMessage("Folder selector is already opened");
|
||||
return;
|
||||
}
|
||||
|
||||
_selecting = true;
|
||||
if(widget.folder) {
|
||||
compute(openFolderPicker, widget.windowTitle)
|
||||
.then((value) => widget.controller.text = value ?? widget.controller.text)
|
||||
.then((_) => _selecting = false);
|
||||
return;
|
||||
}
|
||||
|
||||
compute(openFilePicker, widget.extension!)
|
||||
.then((value) => widget.controller.text = value ?? widget.controller.text)
|
||||
.then((_) => _selecting = false);
|
||||
}
|
||||
}
|
||||
190
lib/src/widget/shared/fluent_card.dart
Normal file
190
lib/src/widget/shared/fluent_card.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class FluentCard extends StatefulWidget {
|
||||
const FluentCard({
|
||||
Key? key,
|
||||
this.leading,
|
||||
required this.content,
|
||||
this.icon,
|
||||
this.trailing,
|
||||
this.animationCurve,
|
||||
this.animationDuration,
|
||||
this.onPressed,
|
||||
this.onStateChanged,
|
||||
this.isButton = false,
|
||||
this.headerHeight = 68.5,
|
||||
this.headerBackgroundColor,
|
||||
this.contentBackgroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
static Color backgroundColor(ThemeData style, Set<ButtonStates> states, [bool isClickable = true]) {
|
||||
if (style.brightness == Brightness.light) {
|
||||
if (!states.isDisabled && isClickable) {
|
||||
if (states.isPressing) return const ColorConst.withOpacity(0xf9f9f9, 0.2);
|
||||
if (states.isHovering) return const ColorConst.withOpacity(0xf9f9f9, 0.4);
|
||||
}
|
||||
return const ColorConst.withOpacity(0xFFFFFF, 0.7);
|
||||
} else {
|
||||
if (!states.isDisabled && isClickable) {
|
||||
if (states.isPressing) return const ColorConst.withOpacity(0xFFFFFF, 0.03);
|
||||
if (states.isHovering) return const ColorConst.withOpacity(0xFFFFFF, 0.082);
|
||||
}
|
||||
return const ColorConst.withOpacity(0xFFFFFF, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
static Color borderColor(ThemeData style, Set<ButtonStates> states, [bool isClickable = true]) {
|
||||
if (style.brightness == Brightness.light) {
|
||||
if (isClickable && states.isHovering && !states.isPressing) return const Color(0xFF212121).withOpacity(0.22);
|
||||
return const Color(0xFF212121).withOpacity(0.17);
|
||||
} else {
|
||||
if (isClickable && states.isPressing) return Colors.white.withOpacity(0.062);
|
||||
if (isClickable && states.isHovering) return Colors.white.withOpacity(0.02);
|
||||
return Colors.black.withOpacity(0.52);
|
||||
}
|
||||
}
|
||||
|
||||
/// The leading widget.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Icon]
|
||||
/// * [RadioButton]
|
||||
/// * [Checkbox]
|
||||
final Widget? leading;
|
||||
|
||||
/// The card content
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget content;
|
||||
|
||||
/// The icon of the toggle button.
|
||||
final Widget? icon;
|
||||
|
||||
/// Disable when onPressed is null, always show chevron icon in the right
|
||||
final bool isButton;
|
||||
|
||||
/// The trailing widget. It's positioned at the right of [content]
|
||||
/// and at the left of [icon].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ToggleSwitch]
|
||||
final Widget? trailing;
|
||||
|
||||
/// Makes the card clickable
|
||||
/// is null by default
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The expand-collapse animation duration. If null, defaults to
|
||||
/// [FluentTheme.fastAnimationDuration]
|
||||
final Duration? animationDuration;
|
||||
|
||||
/// The expand-collapse animation curve. If null, defaults to
|
||||
/// [FluentTheme.animationCurve]
|
||||
final Curve? animationCurve;
|
||||
|
||||
/// A callback called when the current state is changed. `true` when
|
||||
/// open and `false` when closed.
|
||||
final ValueChanged<bool>? onStateChanged;
|
||||
|
||||
/// The height of the header.
|
||||
///
|
||||
/// Defaults to 48.0
|
||||
final double headerHeight;
|
||||
|
||||
/// The background color of the header. If null, [ThemeData.scaffoldBackgroundColor]
|
||||
/// is used
|
||||
final Color? headerBackgroundColor;
|
||||
|
||||
/// The content color of the header. If null, [ThemeData.acrylicBackgroundColor]
|
||||
/// is used
|
||||
final Color? contentBackgroundColor;
|
||||
|
||||
@override
|
||||
FluentCardState createState() => FluentCardState();
|
||||
}
|
||||
|
||||
class FluentCardState extends State<FluentCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late ThemeData theme;
|
||||
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.animationDuration ?? const Duration(milliseconds: 150),
|
||||
);
|
||||
}
|
||||
|
||||
static void emptyPressMethod() {}
|
||||
static const double borderSize = 0.5;
|
||||
static final Color darkBorderColor = Colors.black.withOpacity(0.8);
|
||||
|
||||
static const Duration expanderAnimationDuration = Duration(milliseconds: 70);
|
||||
|
||||
/// If this widget acts as a button and is disabled, gray out all text and icons
|
||||
Widget buttonStyled(Widget child) => !widget.isButton || widget.onPressed != null ? child : IconTheme.merge(
|
||||
data: IconThemeData(color: theme.disabledColor),
|
||||
child: DefaultTextStyle.merge(style: TextStyle(color: theme.disabledColor), child: child)
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final isLtr = Directionality.of(context) == TextDirection.ltr;
|
||||
theme = FluentTheme.of(context);
|
||||
bool isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return buttonStyled(HoverButton(
|
||||
onPressed: widget.onPressed ?? (widget.isButton ? null : emptyPressMethod),
|
||||
builder: (context, states) {
|
||||
return AnimatedContainer(
|
||||
duration: expanderAnimationDuration,
|
||||
height: widget.headerHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: FluentCard.backgroundColor(theme, states, widget.onPressed != null),
|
||||
border: Border.all(
|
||||
width: borderSize,
|
||||
color: FluentCard.borderColor(theme, states, widget.onPressed != null),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.only(start: 16.0),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (widget.leading != null) Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 17.0),
|
||||
child: widget.leading!,
|
||||
),
|
||||
Expanded(child: widget.content),
|
||||
if (widget.trailing != null) Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 20.0, end: 13.5),
|
||||
child: widget.trailing!,
|
||||
),
|
||||
if (widget.icon != null || widget.isButton) Container(
|
||||
margin: EdgeInsetsDirectional.only(
|
||||
start: widget.trailing != null ? 8.0 : 20.0,
|
||||
end: 8.0,
|
||||
top: 8.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: widget.icon ?? Icon(isLtr ? isDark ? FluentIcons.chevron_right : FluentIcons.chevron_right_med :
|
||||
isDark ? FluentIcons.chevron_left : FluentIcons.chevron_left_med, size: 11),
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class ColorConst extends Color {
|
||||
const ColorConst.withOpacity(int value, double opacity) : super(
|
||||
( (((opacity * 0xff ~/ 1) & 0xff) << 24) | ((0x00ffffff & value)) ) & 0xFFFFFFFF);
|
||||
}
|
||||
15
pubspec.yaml
15
pubspec.yaml
@@ -1,6 +1,6 @@
|
||||
name: reboot_launcher
|
||||
description: Launcher for project reboot
|
||||
version: "4.4.0"
|
||||
version: "5.4.0"
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
@@ -13,7 +13,7 @@ dependencies:
|
||||
|
||||
bitsdojo_window:
|
||||
path: ./dependencies/bitsdojo_window-0.1.5
|
||||
fluent_ui: ^4.0.2
|
||||
fluent_ui: ^4.0.3+1
|
||||
bitsdojo_window_windows: ^0.1.5
|
||||
system_theme: ^2.0.0
|
||||
http: ^0.13.5
|
||||
@@ -37,6 +37,11 @@ dependencies:
|
||||
clipboard: ^0.1.3
|
||||
sync: ^0.3.0
|
||||
ini: ^2.1.0
|
||||
universal_disk_space: ^0.2.3
|
||||
jaguar: ^3.1.3
|
||||
hex: ^0.2.0
|
||||
uuid: ^3.0.6
|
||||
dart_vlc: ^0.4.0
|
||||
|
||||
dependency_overrides:
|
||||
win32: ^3.0.0
|
||||
@@ -47,7 +52,6 @@ dev_dependencies:
|
||||
|
||||
flutter_lints: ^2.0.1
|
||||
msix: ^3.6.3
|
||||
hex: ^0.2.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
@@ -55,12 +59,15 @@ flutter:
|
||||
- assets/icons/
|
||||
- assets/binaries/
|
||||
- assets/images/
|
||||
- assets/profiles/
|
||||
- assets/responses/
|
||||
- assets/config/
|
||||
|
||||
msix_config:
|
||||
display_name: Reboot Launcher
|
||||
publisher_display_name: Auties00
|
||||
identity_name: 31868Auties00.RebootLauncher
|
||||
msix_version: 4.4.0.0
|
||||
msix_version: 5.4.0.0
|
||||
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
||||
logo_path: ./assets/icons/reboot.ico
|
||||
architecture: x64
|
||||
|
||||
@@ -40,19 +40,19 @@ Pretty much don't touch this except you really know what you are doing
|
||||
|
||||
### Host
|
||||
|
||||
The host of the remote server to use for the lawin server. Only enabled if you are not using the embedded server.
|
||||
The host of the remote server to use for the backend server. Only enabled if you are not using the embedded server.
|
||||
|
||||
### Port
|
||||
|
||||
The port of the remote server to use for the lawin server. Only enabled if you are not using the embedded server.
|
||||
The port of the remote server to use for the backend server. Only enabled if you are not using the embedded server.
|
||||
|
||||
### Embedded
|
||||
|
||||
A switch to determine whether an embedded lawin server should be used or if you want to use a remote one
|
||||
A switch to determine whether an embedded backend server should be used or if you want to use a remote one
|
||||
|
||||
### Check address / Start or Stop server
|
||||
|
||||
If the embedded switch is off, this button checks that the remote server actually works. If it's on, instead, it will start or stop the lawin server. If you click on the launch button in the launcher page, the server will automatically start if you are using the embedded server and if it's not already running. If the 3551 port is already in use on your pc, the launcher will tell you and provide an option to stop the associated process automatically.
|
||||
If the embedded switch is off, this button checks that the remote backend actually works. If it's on, instead, it will start or stop the lawin server. If you click on the launch button in the launcher page, the server will automatically start if you are using the embedded server and if it's not already running. If the 3551 port is already in use on your pc, the launcher will tell you and provide an option to stop the associated process automatically.
|
||||
|
||||
# Info
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <dart_vlc/dart_vlc_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <system_theme/system_theme_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
@@ -15,6 +16,8 @@
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||
DartVlcPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DartVlcPlugin"));
|
||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||
SystemThemePluginRegisterWithRegistrar(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_windows
|
||||
dart_vlc
|
||||
screen_retriever
|
||||
system_theme
|
||||
url_launcher_windows
|
||||
|
||||
Reference in New Issue
Block a user