This commit is contained in:
Alessandro Autiero
2022-12-16 21:08:57 +01:00
parent ef7f34e0e3
commit 966b4b33fd
91 changed files with 388682 additions and 1005 deletions

View File

@@ -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.

Binary file not shown.

BIN
assets/binaries/leakv2.dll Normal file

Binary file not shown.

View File

@@ -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 ":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

View File

@@ -0,0 +1 @@
sc query "MongoDB" | findstr /i "STATE"

View 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

View 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))

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

103315
assets/profiles/athena.json Normal file

File diff suppressed because it is too large Load Diff

63511
assets/profiles/campaign.json Normal file

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}

View 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
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}

View 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
}

View 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

File diff suppressed because it is too large Load Diff

View 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
}

View 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"
]

View File

@@ -0,0 +1,7 @@
[
"lawin",
"ti93",
"pro100katyt",
"playeereq",
"matteoki"
]

View 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
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,10 @@
{
"friends": [],
"incoming": [],
"outgoing": [],
"suggested": [],
"blocklist": [],
"settings": {
"acceptInvites": "public"
}
}

File diff suppressed because it is too large Load Diff

View 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"
}
]
}

View File

@@ -0,0 +1,4 @@
{
"accountId": "",
"optOutOfPublicLeaderboards": false
}

121549
assets/responses/quests.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.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){ void main(List<String> args){
handleCLI(args); handleCLI(args);
@@ -20,7 +26,7 @@ void main(List<String> args){
Future<void> handleCLI(List<String> args) async { Future<void> handleCLI(List<String> args) async {
stdout.writeln("Reboot Launcher"); stdout.writeln("Reboot Launcher");
stdout.writeln("Wrote by Auties00"); stdout.writeln("Wrote by Auties00");
stdout.writeln("Version 4.4"); stdout.writeln("Version 5.3");
kill(); kill();
@@ -31,16 +37,17 @@ Future<void> handleCLI(List<String> args) async {
var parser = ArgParser() var parser = ArgParser()
..addCommand("list") ..addCommand("list")
..addCommand("launch") ..addCommand("launch")
..addOption("version", defaultsTo: gameJson["version"]) ..addOption("version")
..addOption("username") ..addOption("username")
..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson)) ..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson))
..addOption("server-host") ..addOption("server-host")
..addOption("server-port") ..addOption("server-port")
..addOption("matchmaking-address")
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path) ..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path)
..addOption("type", allowed: getGameTypes(), defaultsTo: getDefaultGameType(gameJson)) ..addOption("type", allowed: getGameTypes(), defaultsTo: getDefaultGameType(gameJson))
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true) ..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
..addFlag("log", defaultsTo: false) ..addFlag("log", defaultsTo: false)
..addFlag("memory-fix", defaultsTo: false, negatable: true); ..addFlag("auto-restart", defaultsTo: false, negatable: true);
var result = parser.parse(args); var result = parser.parse(args);
if (result.command?.name == "list") { if (result.command?.name == "list") {
stdout.writeln("Versions list: "); stdout.writeln("Versions list: ");
@@ -49,45 +56,46 @@ Future<void> handleCLI(List<String> args) async {
return; return;
} }
var dll = result["dll"]; dll = result["dll"];
var type = getGameType(result); type = getGameType(result);
var username = result["username"]; username = result["username"] ?? gameJson["${type == GameType.client ? "game" : "server"}_username"];
username ??= gameJson["${type == GameType.client ? "game" : "server"}_username"]; verbose = result["log"];
var verbose = result["log"];
var dummyVersion = _createVersion(gameJson["version"], result["version"], result["memory-fix"], versions); version = _createVersion(gameJson["version"], result["version"], versions);
await downloadRequiredDLLs(); await downloadRequiredDLLs();
if(result["update"]) { if(result["update"]) {
stdout.writeln("Updating reboot dll..."); 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})..."); stdout.writeln("Launching game(type: ${type.name})...");
if(dummyVersion.executable == null){ if(version.executable == null){
throw Exception("Missing game executable at: ${dummyVersion.location.path}"); throw Exception("Missing game executable at: ${version.location.path}");
} }
if (result["type"] == "headless_server") { await patchHeadless(version.executable!);
await patchHeadless(dummyVersion.executable!); await patchMatchmaking(version.executable!);
}else if(result["type"] == "client"){
await patchMatchmaking(dummyVersion.executable!);
}
var serverType = getServerType(result); var serverType = getServerType(result);
var host = result["server-host"] ?? serverJson["${serverType.id}_host"]; var host = result["server-host"] ?? serverJson["${serverType.id}_host"];
var port = result["server-port"] ?? serverJson["${serverType.id}_port"]; 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){ if(!started){
stderr.writeln("Cannot start server!"); stderr.writeln("Cannot start server!");
return; 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) { if (versionPath != null) {
return FortniteVersion(name: "dummy", location: Directory(versionPath), memoryFix: memoryFix); return FortniteVersion(name: "dummy", location: Directory(versionPath));
} }
if(versionName != null){ if(versionName != null){

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart' import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
show WinDesktopWindow; show WinDesktopWindow;
import 'package:dart_vlc/dart_vlc.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.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/error.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
import 'package:window_manager/window_manager.dart';
final GlobalKey appKey = GlobalKey(); final GlobalKey appKey = GlobalKey();
@@ -28,6 +30,7 @@ void main(List<String> args) async {
} }
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
DartVLC.initialize();
await SystemTheme.accentColor.load(); await SystemTheme.accentColor.load();
await GetStorage.init("game"); await GetStorage.init("game");
@@ -39,11 +42,18 @@ void main(List<String> args) async {
Get.put(BuildController()); Get.put(BuildController());
Get.put(SettingsController()); Get.put(SettingsController());
doWhenWindowReady(() { doWhenWindowReady(() {
const size = Size(600, 365); var controller = Get.find<SettingsController>();
var size = Size(controller.width, controller.height);
var window = appWindow as WinDesktopWindow; var window = appWindow as WinDesktopWindow;
window.setWindowCutOnMaximize(appBarSize * 2); window.setWindowCutOnMaximize(appBarSize * 2);
appWindow.size = size; 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.title = "Reboot Launcher";
appWindow.show(); appWindow.show();
}); });

View 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
View 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
View 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
View 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
View 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(_){
}
}

View File

@@ -4,13 +4,23 @@ import 'package:reboot_launcher/src/model/fortnite_build.dart';
class BuildController extends GetxController { class BuildController extends GetxController {
List<FortniteBuild>? builds; List<FortniteBuild>? builds;
FortniteBuild? _selectedBuild; FortniteBuild? _selectedBuild;
final List<Function()> _listeners;
late RxBool cancelledDownload; late RxBool cancelledDownload;
BuildController() { BuildController() : _listeners = [] {
cancelledDownload = RxBool(false); cancelledDownload = RxBool(false);
} }
FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0); 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();
} }

View File

@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
@@ -90,7 +91,7 @@ class GameController extends GetxController {
_storage.write("version", version?.name); _storage.write("version", version?.name);
} }
void rename(FortniteVersion version, String result) { void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
versions.update((val) => version.name = result); versions.update((val) => function(version));
} }
} }

View File

@@ -3,10 +3,8 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/dialog/server_dialogs.dart'; import 'package:jaguar/jaguar.dart';
import 'package:reboot_launcher/src/util/server.dart';
import '../dialog/snackbar.dart';
import '../model/server_type.dart'; import '../model/server_type.dart';
class ServerController extends GetxController { class ServerController extends GetxController {
@@ -19,9 +17,9 @@ class ServerController extends GetxController {
late final Rx<ServerType> type; late final Rx<ServerType> type;
late final RxBool warning; late final RxBool warning;
late RxBool started; late RxBool started;
late int embeddedServerCounter; Jaguar? embeddedServer;
Process? embeddedServer; Jaguar? embeddedMatchmaker;
HttpServer? reverseProxy; HttpServer? remoteServer;
ServerController() { ServerController() {
_storage = GetStorage("server"); _storage = GetStorage("server");
@@ -37,8 +35,8 @@ class ServerController extends GetxController {
} }
if(value == ServerType.remote){ if(value == ServerType.remote){
reverseProxy?.close(force: true); remoteServer?.close(force: true);
reverseProxy = null; remoteServer = null;
started.value = false; started.value = false;
return; return;
} }
@@ -56,8 +54,6 @@ class ServerController extends GetxController {
warning.listen((value) => _storage.write("lawin_value", value)); warning.listen((value) => _storage.write("lawin_value", value));
started = RxBool(false); started = RxBool(false);
embeddedServerCounter = 0;
} }
String _readHost() { String _readHost() {
@@ -70,83 +66,16 @@ class ServerController extends GetxController {
return _storage.read("${type.value.id}_port") ?? _serverPort; 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 { Future<bool> stop() async {
started.value = false; started.value = false;
try{ try{
switch(type()){ switch(type()){
case ServerType.embedded: case ServerType.embedded:
await freeLawinPort(); await embeddedServer?.close();
await embeddedMatchmaker?.close();
break; break;
case ServerType.remote: case ServerType.remote:
await reverseProxy?.close(force: true); await remoteServer?.close(force: true);
break; break;
case ServerType.local: case ServerType.local:
break; break;

View File

@@ -1,42 +1,41 @@
import 'package:dart_vlc/dart_vlc.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.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/os.dart';
import 'package:reboot_launcher/src/util/server.dart'; import 'dart:ui';
class SettingsController extends GetxController { class SettingsController extends GetxController {
late final GetStorage _storage; late final GetStorage _storage;
late final String originalDll; late final String originalDll;
late final TextEditingController rebootDll; late final TextEditingController rebootDll;
late final TextEditingController consoleDll; late final TextEditingController consoleDll;
late final TextEditingController craniumDll; late final TextEditingController authDll;
late final TextEditingController matchmakingIp; late final TextEditingController matchmakingIp;
late final Rx<PaneDisplayMode> displayType;
late double width;
late double height;
late double? offsetX;
late double? offsetY;
Player? player;
SettingsController() { SettingsController() {
_storage = GetStorage("settings"); _storage = GetStorage("settings");
rebootDll = _createController("reboot", "reboot.dll"); rebootDll = _createController("reboot", "reboot.dll");
consoleDll = _createController("console", "console.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 = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
matchmakingIp.addListener(() async { matchmakingIp.addListener(() async {
var text = matchmakingIp.text; var text = matchmakingIp.text;
_storage.write("ip", 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) { TextEditingController _createController(String key, String name) {
@@ -47,4 +46,14 @@ class SettingsController extends GetxController {
return controller; 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);
}
} }

View File

@@ -15,7 +15,6 @@ class AddLocalVersion extends StatelessWidget {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final TextEditingController _nameController = TextEditingController(); final TextEditingController _nameController = TextEditingController();
final TextEditingController _gamePathController = TextEditingController(); final TextEditingController _gamePathController = TextEditingController();
final CheckboxController _injectMemoryFixController = CheckboxController();
AddLocalVersion({Key? key}) AddLocalVersion({Key? key})
: super(key: key); : super(key: key);
@@ -49,15 +48,6 @@ class AddLocalVersion extends StatelessWidget {
folder: true folder: true
), ),
const SizedBox(
height: 16.0
),
SmartCheckBox(
controller: _injectMemoryFixController,
content: const Text("Inject memory leak fix")
),
const SizedBox(height: 8.0), const SizedBox(height: 8.0),
], ],
), ),
@@ -70,10 +60,10 @@ class AddLocalVersion extends StatelessWidget {
text: "Save", text: "Save",
type: ButtonType.primary, type: ButtonType.primary,
onTap: () { onTap: () {
Navigator.of(context).pop();
_gameController.addVersion(FortniteVersion( _gameController.addVersion(FortniteVersion(
name: _nameController.text, name: _nameController.text,
location: Directory(_gamePathController.text), location: Directory(_gamePathController.text)
memoryFix: _injectMemoryFixController.value
)); ));
}, },
) )

View File

@@ -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/os.dart';
import 'package:reboot_launcher/src/util/build.dart'; import 'package:reboot_launcher/src/util/build.dart';
import 'package:reboot_launcher/src/widget/home/version_name_input.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 '../util/checks.dart';
import '../widget/home/build_selector.dart'; import '../widget/home/build_selector.dart';
import '../widget/shared/file_selector.dart'; import '../widget/shared/file_selector.dart';
import '../widget/shared/smart_check_box.dart';
import 'dialog.dart'; import 'dialog.dart';
class AddServerVersion extends StatefulWidget { class AddServerVersion extends StatefulWidget {
@@ -31,21 +31,29 @@ class _AddServerVersionState extends State<AddServerVersion> {
final BuildController _buildController = Get.find<BuildController>(); final BuildController _buildController = Get.find<BuildController>();
final TextEditingController _nameController = TextEditingController(); final TextEditingController _nameController = TextEditingController();
final TextEditingController _pathController = TextEditingController(); final TextEditingController _pathController = TextEditingController();
final CheckboxController _injectMemoryFixController = CheckboxController();
late Future _future; late DiskSpace _diskSpace;
DownloadStatus _status = DownloadStatus.none; late Future _fetchFuture;
late Future _diskFuture;
DownloadStatus _status = DownloadStatus.form;
String _timeLeft = "00:00:00"; String _timeLeft = "00:00:00";
double _downloadProgress = 0; double _downloadProgress = 0;
String? _error;
Process? _manifestDownloadProcess; Process? _manifestDownloadProcess;
CancelableOperation? _driveDownloadOperation; CancelableOperation? _driveDownloadOperation;
Object? _error;
StackTrace? _stackTrace;
@override @override
void initState() { void initState() {
_future = _buildController.builds != null _fetchFuture = _buildController.builds != null
? Future.value(true) ? Future.value(true)
: compute(fetchBuilds, null) : compute(fetchBuilds, null)
.then((value) => _buildController.builds = value); .then((value) => _buildController.builds = value);
_diskSpace = DiskSpace();
_diskFuture = _diskSpace.scan()
.then((_) => _updateFormDefaults());
_buildController.addOnBuildChangedListener(() => _updateFormDefaults());
super.initState(); super.initState();
} }
@@ -53,6 +61,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
void dispose() { void dispose() {
_pathController.dispose(); _pathController.dispose();
_nameController.dispose(); _nameController.dispose();
_buildController.removeOnBuildChangedListener();
_onDisposed(); _onDisposed();
super.dispose(); super.dispose();
} }
@@ -80,74 +89,58 @@ class _AddServerVersionState extends State<AddServerVersion> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FormDialog( switch(_status){
content: _createDownloadVersionBody(), case DownloadStatus.form:
buttons: _createDownloadVersionOption(context) 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) { List<DialogButton> _createFormButtons() {
switch (_status) { return [
case DownloadStatus.none: DialogButton(type: ButtonType.secondary),
return [ DialogButton(
DialogButton(type: ButtonType.secondary), text: "Download",
DialogButton( type: ButtonType.primary,
text: "Download", onTap: () => _startDownload(context),
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)
];
}
} }
void _startDownload(BuildContext context) async { void _startDownload(BuildContext context) async {
try { try {
setState(() => _status = DownloadStatus.downloading); setState(() => _status = DownloadStatus.downloading);
if (_buildController.selectedBuild.hasManifest) { _manifestDownloadProcess = await downloadManifestBuild(
_manifestDownloadProcess = await downloadManifestBuild( _buildController.selectedBuild.link,
_buildController.selectedBuild.link, _pathController.text,
_pathController.text, _onDownloadProgress
_onDownloadProgress );
); _manifestDownloadProcess!.exitCode
_manifestDownloadProcess!.exitCode .then((value) => _onDownloadComplete());
.then((value) => _onDownloadComplete()); } catch (exception, stackTrace) {
} else { _onDownloadError(exception, stackTrace);
_driveDownloadOperation = CancelableOperation.fromFuture(
downloadArchiveBuild(
_buildController.selectedBuild.link,
_pathController.text,
(progress) => _onDownloadProgress(progress, _timeLeft),
_onUnrar)
).then((_) => _onDownloadComplete(),
onError: (error, _) => _handleError(error));
}
} catch (exception) {
_handleError(exception);
} }
} }
FutureOr? _handleError(Object exception) {
var message = exception.toString();
_onDownloadError(message.contains(":")
? " ${message.substring(message.indexOf(":") + 1)}"
: message);
return null;
}
void _onUnrar() { void _onUnrar() {
setState(() => _status = DownloadStatus.extracting); setState(() => _status = DownloadStatus.extracting);
} }
@@ -161,20 +154,20 @@ class _AddServerVersionState extends State<AddServerVersion> {
_status = DownloadStatus.done; _status = DownloadStatus.done;
_gameController.addVersion(FortniteVersion( _gameController.addVersion(FortniteVersion(
name: _nameController.text, name: _nameController.text,
location: Directory(_pathController.text), location: Directory(_pathController.text)
memoryFix: _injectMemoryFixController.value
)); ));
}); });
} }
void _onDownloadError(String message) { void _onDownloadError(Object? error, StackTrace? stackTrace) {
if (!mounted) { if (!mounted) {
return; return;
} }
setState(() { setState(() {
_status = DownloadStatus.error; _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() { Padding _createExtractingBody() {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
@@ -263,53 +192,77 @@ class _AddServerVersionState extends State<AddServerVersion> {
); );
} }
Column _createDownloadBody() { Widget _createDownloadBody() {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
"Downloading...", "Downloading...",
style: FluentTheme.maybeOf(context)?.typography.body, style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start, textAlign: TextAlign.start,
), ),
), ),
const SizedBox( const SizedBox(
height: 8, 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,
), ),
],
),
const SizedBox( Row(
height: 8, mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: [
Text(
"${_downloadProgress.round()}%",
style: FluentTheme.maybeOf(context)?.typography.body,
),
SizedBox( Text(
width: double.infinity, "Time left: $_timeLeft",
child: ProgressBar(value: _downloadProgress.toDouble())), style: FluentTheme.maybeOf(context)?.typography.body,
const SizedBox( )
height: 16, ],
) ),
],
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( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -327,15 +280,32 @@ class _AddServerVersionState extends State<AddServerVersion> {
validator: checkDownloadDestination, validator: checkDownloadDestination,
folder: true folder: true
), ),
const SizedBox(height: 16.0),
SmartCheckBox(
controller: _injectMemoryFixController,
content: const Text("Inject memory leak fix")
),
const SizedBox(height: 8.0), 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 }

View File

@@ -1,3 +1,4 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -21,12 +22,20 @@ class GenericDialog extends AbstractDialog {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ContentDialog( return Stack(
style: ContentDialogThemeData( children: [
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) 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 { class FutureBuilderDialog extends AbstractDialog {
final Future future; final Future future;
final String loadingMessage; final String loadingMessage;
final Widget loadedBody; final Widget successfulBody;
final Widget unsuccessfulBody;
final Function(Object) errorMessageBuilder; final Function(Object) errorMessageBuilder;
final Function()? onError; final Function()? onError;
final bool closeAutomatically; final bool closeAutomatically;
const FutureBuilderDialog( const FutureBuilderDialog(
{super.key, {super.key,
required this.future, required this.future,
required this.loadingMessage, required this.loadingMessage,
required this.loadedBody, required this.successfulBody,
required this.errorMessageBuilder, required this.unsuccessfulBody,
this.onError, required this.errorMessageBuilder,
this.closeAutomatically = false}); this.onError,
this.closeAutomatically = false});
static Container ofMessage(String message) { static Container ofMessage(String message) {
return Container( return Container(
@@ -170,31 +181,43 @@ class FutureBuilderDialog extends AbstractDialog {
Widget _createBody(BuildContext context, AsyncSnapshot snapshot){ Widget _createBody(BuildContext context, AsyncSnapshot snapshot){
if (snapshot.hasError) { if (snapshot.hasError) {
onError?.call(); 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) { if (!snapshot.hasData) {
return InfoLabel( return _createLoadingBody();
label: loadingMessage,
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
width: double.infinity,
child: const ProgressBar()),
);
} }
if(closeAutomatically){ 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){ DialogButton _createButton(BuildContext context, AsyncSnapshot snapshot){
return DialogButton( return DialogButton(
text: snapshot.hasData || snapshot.hasError ? "Close" : "Stop", text: snapshot.hasData
type: ButtonType.only, || snapshot.hasError
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData) || (snapshot.connectionState == ConnectionState.done && snapshot.data == null) ? "Close" : "Stop",
type: ButtonType.only,
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
); );
} }
} }

View File

@@ -6,7 +6,7 @@ Future<void> showBrokenError() async {
showDialog( showDialog(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => const InfoDialog( 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( showDialog(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => const InfoDialog( 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( showDialog(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => const InfoDialog( 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."
) )
); );
} }

View File

@@ -1,12 +1,15 @@
import 'package:fluent_ui/fluent_ui.dart'; 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/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.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart'; import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/snackbar.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/model/server_type.dart';
import 'package:reboot_launcher/src/util/future.dart';
import 'package:sync/semaphore.dart'; import 'package:sync/semaphore.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../main.dart'; import '../../main.dart';
import '../util/server.dart'; import '../util/server.dart';
@@ -14,26 +17,31 @@ import '../util/server.dart';
extension ServerControllerDialog on ServerController { extension ServerControllerDialog on ServerController {
static Semaphore semaphore = Semaphore(); 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{ try{
semaphore.acquire(); semaphore.acquire();
if (type() == ServerType.local) { if (type() == ServerType.local) {
return _checkLocalServerInteractive(onlyIfNeeded); return _pingSelfInteractive(required);
} }
var oldStarted = started(); var oldStarted = started();
if(oldStarted && onlyIfNeeded){ if(oldStarted && required){
return true; return true;
} }
started.value = !started.value; 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{ }finally{
semaphore.release(); 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) { if (oldStarted) {
var result = await stop(); var result = await stop();
if (!result) { if (!result) {
@@ -45,13 +53,14 @@ extension ServerControllerDialog on ServerController {
return false; return false;
} }
var result = await start(!onlyIfNeeded); var conditions = await checkServerPreconditions(host.text, port.text, type.value, !required);
if(result.type == ServerResultType.ignoreStart) { var result = conditions.type == ServerResultType.canStart ? await _startServer(required) : conditions;
if(result.type == ServerResultType.alreadyStarted) {
started.value = false; started.value = false;
return true; return true;
} }
var handled = await _handleResultType(oldStarted, onlyIfNeeded, isRetry, result); var handled = await _handleResultType(oldStarted, required, isRetry, askPortKill, result);
if (!handled) { if (!handled) {
started.value = false; started.value = false;
return false; return false;
@@ -60,7 +69,42 @@ extension ServerControllerDialog on ServerController {
return handled; 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) { switch (result.type) {
case ServerResultType.missingHostError: case ServerResultType.missingHostError:
_showMissingHostError(); _showMissingHostError();
@@ -72,11 +116,6 @@ extension ServerControllerDialog on ServerController {
_showIllegalPortError(); _showIllegalPortError();
return false; return false;
case ServerResultType.cannotPingServer: case ServerResultType.cannotPingServer:
if(!started() || result.pid != embeddedServer?.pid){
return false;
}
_showPingErrorDialog();
return false; return false;
case ServerResultType.portTakenError: case ServerResultType.portTakenError:
if (isRetry) { if (isRetry) {
@@ -84,24 +123,15 @@ extension ServerControllerDialog on ServerController {
return false; return false;
} }
var result = await _showPortTakenDialog(); if(askPortKill) {
if (!result) { var result = await _showPortTakenDialog();
return false; if (!result) {
return false;
}
} }
await freeLawinPort(); await freeLawinPort();
return _doStateChange(oldStarted, onlyIfNeeded, true); return _startInternal(oldStarted, onlyIfNeeded, askPortKill, true);
case ServerResultType.serverDownloadRequiredError:
if (isRetry) {
return false;
}
var result = await downloadServerInteractive(false);
if (!result) {
return false;
}
return _doStateChange(oldStarted, onlyIfNeeded, true);
case ServerResultType.unknownError: case ServerResultType.unknownError:
showDialog( showDialog(
context: appKey.currentContext!, context: appKey.currentContext!,
@@ -114,39 +144,67 @@ extension ServerControllerDialog on ServerController {
) )
); );
return false; return false;
case ServerResultType.ignoreStart: case ServerResultType.alreadyStarted:
case ServerResultType.started:
return true;
case ServerResultType.canStart: case ServerResultType.canStart:
return true;
case ServerResultType.stopped: case ServerResultType.stopped:
return false; return false;
} }
} }
Future<bool> _checkLocalServerInteractive(bool ignorePrompts) async { Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
try { try {
var future = pingSelf(port.text); return await showDialog<bool>(
if(!ignorePrompts) { context: appKey.currentContext!,
await showDialog( builder: (context) =>
context: appKey.currentContext!, FutureBuilderDialog(
builder: (context) => future: Future.wait([
FutureBuilderDialog( pingSelf(port.text),
future: future, Future.delayed(const Duration(seconds: 1))
loadingMessage: "Pinging server...", ]),
loadedBody: FutureBuilderDialog.ofMessage( loadingMessage: "Pinging ${type().id} server...",
"The server at ${host.text}:${port successfulBody: FutureBuilderDialog.ofMessage(
.text} works correctly"), "The ${type().id} server works correctly"),
errorMessageBuilder: ( unsuccessfulBody: FutureBuilderDialog.ofMessage(
exception) => "An error occurred while pining the server: $exception" "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
return await future != null; )
) ?? false;
} catch (_) { } catch (_) {
return false; 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 { Future<void> _showPortTakenError() async {
showDialog( showDialog(
context: appKey.currentContext!, context: appKey.currentContext!,
@@ -178,20 +236,6 @@ extension ServerControllerDialog on ServerController {
) ?? false; ) ?? 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() { void _showCannotStopError() {
if(!started.value){ if(!started.value){
return; return;
@@ -201,47 +245,45 @@ extension ServerControllerDialog on ServerController {
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => builder: (context) =>
const InfoDialog( const InfoDialog(
text: "Cannot stop lawin server" text: "Cannot stop backend server"
) )
); );
} }
void showUnexpectedError() { void showUnexpectedServerError() {
showDialog( showDialog(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => builder: (context) => InfoDialog(
const InfoDialog( text: "The backend server died unexpectedly",
text: "The lawin 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() { void _showIllegalPortError() {
showMessage("Illegal port for lawin server, use only numbers"); showMessage("Illegal port for backend server, use only numbers");
} }
void _showMissingPortError() { void _showMissingPortError() {
showMessage("Missing port for lawin server"); showMessage("Missing port for backend server");
} }
void _showMissingHostError() { 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
View 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");
}

View 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"
}
);
}
}

View 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"
}
}
];

View 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

File diff suppressed because it is too large Load Diff

View 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"]
};
}

View 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;
}

View 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");
}

View 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) => {};

View 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;
}

View 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"
};

View File

@@ -3,7 +3,6 @@ import 'package:version/version.dart';
class FortniteBuild { class FortniteBuild {
final Version version; final Version version;
final String link; final String link;
final bool hasManifest;
FortniteBuild({required this.version, required this.link, required this.hasManifest}); FortniteBuild({required this.version, required this.link});
} }

View File

@@ -5,14 +5,12 @@ import 'package:path/path.dart' as path;
class FortniteVersion { class FortniteVersion {
String name; String name;
Directory location; Directory location;
bool memoryFix;
FortniteVersion.fromJson(json) FortniteVersion.fromJson(json)
: name = json["name"], : name = json["name"],
location = Directory(json["location"]), location = Directory(json["location"]);
memoryFix = json["memory_fix"] ?? false;
FortniteVersion({required this.name, required this.location, required this.memoryFix}); FortniteVersion({required this.name, required this.location});
static File? findExecutable(Directory directory, String name) { static File? findExecutable(Directory directory, String name) {
try{ try{
@@ -32,13 +30,17 @@ class FortniteVersion {
return findExecutable(location, "FortniteLauncher.exe"); return findExecutable(location, "FortniteLauncher.exe");
} }
File? get eacExecutable {
return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
}
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'name': name, 'name': name,
'location': location.path, 'location': location.path
}; };
@override @override
String toString() { String toString() {
return 'FortniteVersion{name: $name, location: $location}'; return 'FortniteVersion{name: $name, location: $location';
} }
} }

View File

@@ -1,6 +1,14 @@
import 'dart:convert';
import 'dart:ui';
import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder; import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
import 'package:fluent_ui/fluent_ui.dart'; 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/settings_page.dart';
import 'package:reboot_launcher/src/page/launcher_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart';
import 'package:reboot_launcher/src/page/server_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:reboot_launcher/src/widget/os/window_buttons.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../controller/settings_controller.dart';
import 'info_page.dart'; import 'info_page.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
@@ -20,117 +29,294 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WindowListener { class _HomePageState extends State<HomePage> with WindowListener {
static const double _headerSize = 48.0; 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 _headerButtonCount = 3;
static const int _sectionButtonCount = 4; 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; bool _shouldMaximize = false;
int _index = 0;
@override @override
void initState() { void initState() {
windowManager.addListener(this); 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(); super.initState();
} }
@override @override
void dispose() { void dispose() {
windowManager.removeListener(this); windowManager.removeListener(this);
_searchFocusNode.dispose();
_searchController.dispose();
super.dispose(); super.dispose();
} }
@override @override
void onWindowFocus() { void onWindowFocus() {
setState(() => _focused = true); _focused.value = true;
} }
@override @override
void onWindowBlur() { void onWindowBlur() {
setState(() => _focused = !_focused); _focused.value = false;
} }
@override @override
Widget build(BuildContext context) { void onWindowMoved() {
return Stack( _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: [ children: [
NavigationView( SizedBox(
pane: NavigationPane( width: _settingsController.displayType() == PaneDisplayMode.open ? _openMenuSize : _headerSize,
size: const NavigationPaneSize( height: _headerSize
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()
)
]
),
), ),
_createTitleBar(), Expanded(
child: _createWindowGestures(
_createGestureHandler(), child: Container(
height: _headerSize,
if(_focused && isWin11) color: Colors.transparent
const WindowBorder() )
)
),
WindowTitleBar(focused: _focused())
], ],
); );
} }
Align _createTitleBar() { String get searchValue => _searchController.text;
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()
),
),
);
}
} }

View File

@@ -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:flutter/foundation.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.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); const InfoPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { State<InfoPage> createState() => _InfoPageState();
return Padding( }
padding: const EdgeInsets.all(12.0),
child: SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
_createVersionInfo(),
Column( class _InfoPageState extends State<InfoPage> {
mainAxisAlignment: MainAxisAlignment.center, final SettingsController _settingsController = Get.find<SettingsController>();
mainAxisSize: MainAxisSize.max,
children: [ @override
_createAutiesAvatar(), void initState() {
const SizedBox( if(_settingsController.player == null){
height: 16.0, var player = Player(id: 1);
), player.open(
const Text("Made by Auties00"), Media.network("https://cdn.discordapp.com/attachments/1006260074416701450/1038844107986055190/tutorial.mp4")
const SizedBox( );
height: 16.0, _settingsController.player = player;
), }
_createDiscordButton()
], _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' : ''}")
);
}
} }

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -6,13 +5,12 @@ import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart'; import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_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/game_type_selector.dart';
import 'package:reboot_launcher/src/widget/home/launch_button.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/username_box.dart';
import 'package:reboot_launcher/src/widget/home/version_selector.dart'; import 'package:reboot_launcher/src/widget/home/version_selector.dart';
import '../controller/settings_controller.dart';
import '../util/reboot.dart'; import '../util/reboot.dart';
class LauncherPage extends StatefulWidget { class LauncherPage extends StatefulWidget {
@@ -27,7 +25,6 @@ class LauncherPage extends StatefulWidget {
class _LauncherPageState extends State<LauncherPage> { class _LauncherPageState extends State<LauncherPage> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final BuildController _buildController = Get.find<BuildController>(); final BuildController _buildController = Get.find<BuildController>();
final SettingsController _settingsController = Get.find<SettingsController>();
@override @override
void initState() { void initState() {
@@ -43,12 +40,12 @@ class _LauncherPageState extends State<LauncherPage> {
int? get _updateTime { int? get _updateTime {
var storage = GetStorage("update"); var storage = GetStorage("update");
return storage.read("last_update"); return storage.read("last_update_v2");
} }
set _updateTime(int? updateTime) { set _updateTime(int? updateTime) {
var storage = GetStorage("update"); var storage = GetStorage("update");
storage.write("last_update", updateTime); storage.write("last_update_v2", updateTime);
} }
void _onCancelWarning() { void _onCancelWarning() {
@@ -65,51 +62,63 @@ class _LauncherPageState extends State<LauncherPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return FutureBuilder(
padding: const EdgeInsets.all(12.0), future: _gameController.updater ?? Future.value(true),
child: FutureBuilder( builder: (context, snapshot) {
future: _gameController.updater ?? Future.value(true), if (!snapshot.hasData && !snapshot.hasError) {
builder: (context, snapshot) { return Row(
if (!snapshot.hasData && !snapshot.hasError) { mainAxisAlignment: MainAxisAlignment.center,
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,
children: [ children: [
if(snapshot.hasError) Column(
_createUpdateError(snapshot), mainAxisAlignment: MainAxisAlignment.center,
UsernameBox(), children: const [
const VersionSelector(), ProgressRing(),
GameTypeSelector(), SizedBox(height: 16.0),
const LaunchButton() 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) { Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
return const SizedBox( return MouseRegion(
width: double.infinity, cursor: SystemMouseCursors.click,
child: InfoBar( child: GestureDetector(
title: Text("Cannot update dll"), onTap: () {
severity: InfoBarSeverity.warning 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
),
)
), ),
); );
} }
} }

View File

@@ -13,27 +13,24 @@ class ServerPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Obx(() => Column(
padding: const EdgeInsets.all(12.0), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Obx(() => Column( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
crossAxisAlignment: CrossAxisAlignment.start, if(_serverController.warning.value)
children: [ SizedBox(
if(_serverController.warning.value) width: double.infinity,
SizedBox( child: InfoBar(
width: double.infinity, title: const Text("The backend server handles authentication and parties, not game hosting"),
child: InfoBar( severity: InfoBarSeverity.info,
title: const Text("The lawin server handles authentication and parties, not game hosting"), onClose: () => _serverController.warning.value = false
severity: InfoBarSeverity.info,
onClose: () => _serverController.warning.value = false
),
), ),
HostInput(), ),
PortInput(), HostInput(),
ServerTypeSelector(), PortInput(),
const ServerButton() ServerTypeSelector(),
] const ServerButton()
)), ]
); ));
} }
} }

View File

@@ -19,60 +19,57 @@ class SettingsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Column(
padding: const EdgeInsets.all(12.0), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
crossAxisAlignment: CrossAxisAlignment.start, Tooltip(
children: [ message: "The hostname of the server that hosts the multiplayer matches",
Tooltip( child: Obx(() => SmartInput(
message: "The hostname of the server that hosts the multiplayer matches", label: "Matchmaking Host",
child: Obx(() => SmartInput( placeholder:
label: "Matchmaking Host", "Type the hostname of the server that hosts the multiplayer matches",
placeholder: controller: _settingsController.matchmakingIp,
"Type the hostname of the server that hosts the multiplayer matches", validatorMode: AutovalidateMode.always,
controller: _settingsController.matchmakingIp, validator: checkMatchmaking,
validatorMode: AutovalidateMode.always, enabled: _serverController.type() == ServerType.embedded
validator: checkMatchmaking, ))
enabled: _serverController.type() == ServerType.embedded ),
)) Tooltip(
), message: "The dll that is injected when a server is launched",
Tooltip( child: FileSelector(
message: "The dll that is injected when a server is launched", 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( child: FileSelector(
label: "Reboot DLL", label: "Cranium DLL",
placeholder: "Type the path to the reboot dll", placeholder: "Type the path to the dll used for authentication",
controller: _settingsController.rebootDll, controller: _settingsController.authDll,
windowTitle: "Select a dll", windowTitle: "Select a dll",
folder: false, folder: false,
extension: "dll", extension: "dll",
validator: checkDll, validator: checkDll,
validatorMode: AutovalidateMode.always), 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))
]),
);
} }
} }

View File

@@ -5,51 +5,20 @@ import 'package:html/parser.dart' show parse;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:process_run/shell.dart'; import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/model/fortnite_build.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:reboot_launcher/src/util/version.dart' as parser;
import 'package:version/version.dart';
import 'os.dart'; import 'os.dart';
const _userAgent = 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"; "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( final _manifestSourceUrl = Uri.parse(
"https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md"); "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 { 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); var response = await http.get(_manifestSourceUrl);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}"); throw Exception("Erroneous status code: ${response.statusCode}");
@@ -68,13 +37,13 @@ Future<List<FortniteBuild>> _fetchManifests() async {
var name = children[0].text; var name = children[0].text;
var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-")); var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-"));
var version = parser var version = parser
.tryParse(minifiedName.replaceFirst("-CL", "")); .tryParse(minifiedName.replaceFirst("", ""));
if (version == null) { if (version == null) {
continue; continue;
} }
var link = children[2].firstChild!.attributes["href"]!; 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; return results;
@@ -93,9 +62,12 @@ Future<Process> downloadManifestBuild(
} }
Future<void> downloadArchiveBuild(String archiveUrl, String destination, 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( var tempFile = File(
"$destination\\.temp\\FortniteBuild${Random.secure().nextInt(1000000)}.rar"); "$destination\\.temp\\FortniteBuild$uuid$extension"
);
await tempFile.parent.create(recursive: true); await tempFile.parent.create(recursive: true);
try { try {
var client = http.Client(); var client = http.Client();
@@ -106,27 +78,38 @@ Future<void> downloadArchiveBuild(String archiveUrl, String destination,
throw Exception("Erroneous status code: ${response.statusCode}"); throw Exception("Erroneous status code: ${response.statusCode}");
} }
var startTime = DateTime.now();
var lastRemaining = -1;
var length = response.contentLength!; var length = response.contentLength!;
var received = 0; var received = 0;
var sink = tempFile.openWrite(); var sink = tempFile.openWrite();
await response.stream.map((s) { var lastEta = toETA(0);
received += s.length; await response.stream.map((entry) {
onProgress((received / length) * 100); received += entry.length;
return s; 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); }).pipe(sink);
onRar(); onDecompress();
var output = Directory(destination); var output = Directory(destination);
await output.create(recursive: true); await output.create(recursive: true);
await loadBinary("winrar.exe", true);
var shell = Shell( var shell = Shell(
commandVerbose: false, commandVerbose: false,
commentVerbose: 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 { } finally {
if (await tempFile.exists()) { if (await tempFile.parent.exists()) {
tempFile.delete(); tempFile.parent.delete(recursive: true);
} }
} }
} }

24
lib/src/util/error.dart Normal file
View 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"
)
);
}

View File

@@ -1,5 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:win32/win32.dart';
import 'package:ffi/ffi.dart';
import 'dart:ffi';
const int appBarSize = 2; const int appBarSize = 2;
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))'); final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -31,6 +35,18 @@ Future<File> loadBinary(String binary, bool safe) async{
return safeBinary; 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){ File _locateInternalBinary(String binary){
return File("$internalBinariesDirectory\\$binary"); return File("$internalBinariesDirectory\\$binary");
} }
@@ -42,4 +58,13 @@ Directory get tempDirectory =>
Directory("${Platform.environment["Temp"]}"); Directory("${Platform.environment["Temp"]}");
String get safeBinariesDirectory => String get safeBinariesDirectory =>
"${Platform.environment["UserProfile"]}\\.reboot_launcher"; "${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");
}

View File

@@ -7,7 +7,7 @@ import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
const _rebootUrl = 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 { Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) : null; return lastUpdateMs != null ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) : null;
@@ -29,14 +29,18 @@ Future<int> downloadRebootDll(int? lastUpdateMs) async {
var outputDir = await tempDirectory.createTemp("reboot"); var outputDir = await tempDirectory.createTemp("reboot");
await extractFileToDisk(tempZip.path, outputDir.path); await extractFileToDisk(tempZip.path, outputDir.path);
var rebootDll = outputDir.listSync() var rebootDll = File(
.firstWhere((element) => path.extension(element.path) == ".dll"); outputDir.listSync()
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) { .firstWhere((element) => path.extension(element.path) == ".dll")
.path
);
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await rebootDll.readAsBytes())) {
outputDir.delete(recursive: true); outputDir.delete(recursive: true);
return now.millisecondsSinceEpoch; return now.millisecondsSinceEpoch;
} }
await rebootDll.rename(oldRebootDll.path); await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
outputDir.delete(recursive: true); outputDir.delete(recursive: true);
return now.millisecondsSinceEpoch; return now.millisecondsSinceEpoch;
} }

View 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;
}

View File

@@ -1,29 +1,15 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:process_run/shell.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/model/server_type.dart';
import 'package:reboot_launcher/src/util/os.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_proxy/shelf_proxy.dart';
import 'package:shelf/shelf_io.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"); 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 { Future<bool> isLawinPortFree() async {
try { try {
var portBat = await loadBinary("port.bat", true); var portBat = await loadBinary("port.bat", true);
@@ -39,27 +25,41 @@ Future<bool> isLawinPortFree() async {
Future<void> freeLawinPort() async { Future<void> freeLawinPort() async {
var releaseBat = await loadBinary("release.bat", false); 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 = [ var args = [
"-skippatchcheck",
"-epicapp=Fortnite", "-epicapp=Fortnite",
"-epicenv=Prod", "-epicenv=Prod",
"-epiclocale=en-us", "-epiclocale=en-us",
"-epicportal", "-epicportal",
"-noeac", "-skippatchcheck",
"-fromfl=be", "-nobe",
"-fltoken=7ce411021b27b4343a44fdg8", "-fromfl=eac",
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", "-fltoken=3db3ba5dcbd2e16703f3978d",
"-AUTH_LOGIN=$username@projectreboot.dev", "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ"
"-AUTH_PASSWORD=Rebooted",
"-AUTH_TYPE=epic"
]; ];
if(headless){ if(username.isNotEmpty){
args.addAll(["-nullrhi", "-nosplash", "-nosound"]); 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; return args;
@@ -118,8 +118,7 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
if (!free) { if (!free) {
if(!needsFreePort) { if(!needsFreePort) {
return ServerResult( return ServerResult(
uri: pingSelf(port), type: ServerResultType.alreadyStarted
type: ServerResultType.ignoreStart
); );
} }
@@ -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( return ServerResult(
uri: ping(host, port),
type: ServerResultType.canStart 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 { Future<HttpServer> startRemoteServer(Uri uri) async {
return await serve(proxyHandler(uri), "127.0.0.1", 3551); 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 { class ServerResult {
final Future<Uri?>? uri;
final int? pid; final int? pid;
final Object? error; final Object? error;
final StackTrace? stackTrace; final StackTrace? stackTrace;
final ServerResultType type; 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 { enum ServerResultType {
@@ -185,10 +152,8 @@ enum ServerResultType {
illegalPortError, illegalPortError,
cannotPingServer, cannotPingServer,
portTakenError, portTakenError,
serverDownloadRequiredError,
canStart, canStart,
ignoreStart, alreadyStarted,
started,
unknownError, unknownError,
stopped, stopped,
} }

47
lib/src/util/time.dart Normal file
View 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";
}
}

View File

@@ -37,9 +37,8 @@ class _BuildSelectorState extends State<BuildSelector> {
ComboBoxItem<FortniteBuild> _createItem(FortniteBuild element) { ComboBoxItem<FortniteBuild> _createItem(FortniteBuild element) {
return ComboBoxItem<FortniteBuild>( return ComboBoxItem<FortniteBuild>(
value: element, value: element,
child: Text( child: Text(element.version.toString())
"${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"),
); );
} }
} }

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:clipboard/clipboard.dart'; import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.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/game_controller.dart';
import 'package:reboot_launcher/src/controller/server_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.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/game_dialogs.dart';
import 'package:reboot_launcher/src/dialog/server_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/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/os.dart';
import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/patcher.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:win32_suspend_process/win32_suspend_process.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../../../main.dart';
import '../../controller/settings_controller.dart'; import '../../controller/settings_controller.dart';
import '../../dialog/snackbar.dart'; import '../../dialog/snackbar.dart';
@@ -34,6 +35,15 @@ class LaunchButton extends StatefulWidget {
} }
class _LaunchButtonState extends State<LaunchButton> { 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 GameController _gameController = Get.find<GameController>();
final ServerController _serverController = Get.find<ServerController>(); final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
@@ -70,13 +80,13 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
_gameController.started.value = true; if (_gameController.username.text.isEmpty && _gameController.type() != GameType.client) {
if (_gameController.username.text.isEmpty) { showMessage("Missing username");
showMessage("Missing in-game username");
_gameController.started.value = false; _gameController.started.value = false;
return; return;
} }
_gameController.started.value = true;
if (_gameController.selectedVersionObs.value == null) { if (_gameController.selectedVersionObs.value == null) {
showMessage("No version is selected"); showMessage("No version is selected");
_gameController.started.value = false; _gameController.started.value = false;
@@ -87,7 +97,7 @@ class _LaunchButtonState extends State<LaunchButton> {
var version = _gameController.selectedVersionObs.value!; var version = _gameController.selectedVersionObs.value!;
var gamePath = version.executable?.path; var gamePath = version.executable?.path;
if(gamePath == null){ 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(); _onStop();
return; return;
} }
@@ -97,8 +107,17 @@ class _LaunchButtonState extends State<LaunchButton> {
Win32Process(_gameController.launcherProcess!.pid).suspend(); 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){ if(!result){
showMessage("Cannot launch the game as the backend didn't start up correctly");
_onStop(); _onStop();
return; return;
} }
@@ -107,15 +126,16 @@ class _LaunchButtonState extends State<LaunchButton> {
await _logFile!.delete(); await _logFile!.delete();
} }
await compute(patchMatchmaking, version.executable!);
await patch(version.executable!); await compute(patchHeadless, version.executable!);
var headlessHosting = _gameController.type() == GameType.headlessServer; 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) _gameController.gameProcess = await Process.start(gamePath, arguments)
..exitCode.then((_) => _onEnd()) ..exitCode.then((_) => _onEnd())
..outLines.forEach((line) => _onGameOutput(line, version.memoryFix)) ..outLines.forEach((line) => _onGameOutput(line))
..errLines.forEach((line) => _onGameOutput(line, version.memoryFix)); ..errLines.forEach((line) => _onGameOutput(line));
_injectOrShowError(Injectable.cranium);
if(headlessHosting){ if(headlessHosting){
await _showServerLaunchingWarning(); 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() { void _onEnd() {
if(_fail){ if(_fail){
return; return;
@@ -147,25 +156,17 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
void _closeDialogIfOpen(bool success) { void _closeDialogIfOpen(bool success) {
if(!mounted){ var route = ModalRoute.of(appKey.currentContext!);
return;
}
var route = ModalRoute.of(context);
if(route == null || route.isCurrent){ if(route == null || route.isCurrent){
return; return;
} }
Navigator.of(context).pop(success); Navigator.of(appKey.currentContext!).pop(success);
} }
Future<void> _showServerLaunchingWarning() async { Future<void> _showServerLaunchingWarning() async {
if(!mounted){
return;
}
var result = await showDialog<bool>( var result = await showDialog<bool>(
context: context, context: appKey.currentContext!,
builder: (context) => ProgressDialog( builder: (context) => ProgressDialog(
text: "Launching headless server...", text: "Launching headless server...",
onStop: () { onStop: () {
@@ -182,11 +183,7 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop(); _onStop();
} }
void _onGameOutput(String line, bool memoryFix) { void _onGameOutput(String line) {
if(kDebugMode){
print(line);
}
if(_logFile != null){ if(_logFile != null){
_logFile!.writeAsString("$line\n", mode: FileMode.append); _logFile!.writeAsString("$line\n", mode: FileMode.append);
} }
@@ -196,33 +193,18 @@ class _LaunchButtonState extends State<LaunchButton> {
return; 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; _fail = true;
_closeDialogIfOpen(false); _closeDialogIfOpen(false);
showBrokenError(); _showTokenError();
return; return;
} }
if(line.contains("HTTP 400 response from ")){ if(line.contains("Region ")){
_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(_gameController.type.value == GameType.client){ if(_gameController.type.value == GameType.client){
_injectOrShowError(Injectable.console); _injectOrShowError(Injectable.console);
}else { }else {
@@ -230,9 +212,19 @@ class _LaunchButtonState extends State<LaunchButton> {
.then((value) => _closeDialogIfOpen(true)); .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); await injectDll(gameProcess.pid, dllPath.path);
} catch (exception) { } catch (exception) {
showSnackbar( showSnackbar(
context, appKey.currentContext!,
Snackbar( Snackbar(
content: Text("Cannot inject $injectable.dll: $exception", textAlign: TextAlign.center), content: Text("Cannot inject $injectable.dll: $exception", textAlign: TextAlign.center),
extended: true extended: true
@@ -283,6 +275,10 @@ class _LaunchButtonState extends State<LaunchButton> {
void _onDllFail(File dllPath) { void _onDllFail(File dllPath) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if(_fail){
return;
}
_fail = true; _fail = true;
_closeDialogIfOpen(false); _closeDialogIfOpen(false);
showMissingDllError(path.basename(dllPath.path)); showMissingDllError(path.basename(dllPath.path));
@@ -297,9 +293,9 @@ class _LaunchButtonState extends State<LaunchButton> {
case Injectable.console: case Injectable.console:
return File(_settingsController.consoleDll.text); return File(_settingsController.consoleDll.text);
case Injectable.cranium: case Injectable.cranium:
return File(_settingsController.craniumDll.text); return File(_settingsController.authDll.text);
case Injectable.memoryFix: case Injectable.memoryFix:
return await loadBinary("fix.dll", true); return await loadBinary("leakv2.dll", true);
} }
} }

View File

@@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.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/model/fortnite_version.dart';
import 'package:reboot_launcher/src/dialog/add_local_version.dart'; import 'package:reboot_launcher/src/dialog/add_local_version.dart';
import 'package:reboot_launcher/src/widget/shared/smart_check_box.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 '../../dialog/add_server_version.dart';
import '../../util/checks.dart'; import '../../util/checks.dart';
import '../shared/file_selector.dart';
class VersionSelector extends StatefulWidget { class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key); const VersionSelector({Key? key}) : super(key: key);
@@ -58,9 +61,7 @@ class _VersionSelectorState extends State<VersionSelector> {
Widget _createSelector(BuildContext context) { Widget _createSelector(BuildContext context) {
return Tooltip( return Tooltip(
message: "The version of Fortnite to launch", message: "The version of Fortnite to launch",
child: SizedBox( child: Obx(() => DropDownButton(
width: double.infinity,
child: Obx(() => DropDownButton(
leading: Text(_gameController.selectedVersionObs.value?.name ?? leading: Text(_gameController.selectedVersionObs.value?.name ??
"Select a version"), "Select a version"),
items: _gameController.hasNoVersions items: _gameController.hasNoVersions
@@ -68,28 +69,26 @@ class _VersionSelectorState extends State<VersionSelector> {
: _gameController.versions.value : _gameController.versions.value
.map((version) => _createVersionItem(context, version)) .map((version) => _createVersionItem(context, version))
.toList())) .toList()))
),
); );
} }
MenuFlyoutItem _createVersionItem( MenuFlyoutItem _createVersionItem(
BuildContext context, FortniteVersion version) { BuildContext context, FortniteVersion version) {
return MenuFlyoutItem( return MenuFlyoutItem(
text: SizedBox( text: Listener(
width: double.infinity, onPointerDown: (event) async {
child: Listener( if (event.kind != PointerDeviceKind.mouse ||
onPointerDown: (event) async { event.buttons != kSecondaryMouseButton) {
if (event.kind != PointerDeviceKind.mouse || return;
event.buttons != kSecondaryMouseButton) { }
return;
}
await _openMenu(context, version, event.position); await _openMenu(context, version, event.position);
}, },
child: Text(version.name) child: SizedBox(
) width: double.infinity,
child: Text(version.name)
),
), ),
trailing: const Expanded(child: SizedBox()),
onPressed: () => _gameController.selectedVersion = version); onPressed: () => _gameController.selectedVersion = version);
} }
@@ -120,7 +119,7 @@ class _VersionSelectorState extends State<VersionSelector> {
context: context, context: context,
offset: offset, offset: offset,
builder: (context) => MenuFlyout( builder: (context) => MenuFlyout(
items: ContextualOption.getValues(version.memoryFix) items: ContextualOption.values
.map((entry) => _createOption(context, entry)) .map((entry) => _createOption(context, entry))
.toList() .toList()
) )
@@ -137,18 +136,13 @@ class _VersionSelectorState extends State<VersionSelector> {
.onError((error, stackTrace) => _onExplorerError()); .onError((error, stackTrace) => _onExplorerError());
break; break;
case ContextualOption.rename: case ContextualOption.modify:
if(!mounted){ if(!mounted){
return; return;
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
var result = await _openRenameDialog(context, version); await _openRenameDialog(context, version);
if(result == null){
return;
}
_gameController.rename(version, result);
break; break;
case ContextualOption.delete: case ContextualOption.delete:
@@ -173,24 +167,6 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
break; 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: default:
break; break;
@@ -251,46 +227,57 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) { 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?>( return showDialog<String?>(
context: context, context: context,
builder: (context) => Form( builder: (context) => FormDialog(
child: Builder( content: Column(
builder: (context) => ContentDialog( mainAxisSize: MainAxisSize.min,
content: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
crossAxisAlignment: CrossAxisAlignment.start, TextFormBox(
children: [ controller: nameController,
TextFormBox( header: "Name",
controller: controller, placeholder: "Type the new version name",
header: "Name", autofocus: true,
placeholder: "Type the new version name", validator: (text) => checkVersion(text, _gameController.versions.value)
autofocus: true, ),
validator: (text) => checkVersion(text, _gameController.versions.value)
),
const SizedBox(height: 8.0), const SizedBox(
], height: 16.0
), ),
actions: [
Button(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('Close'),
),
FilledButton(
onPressed: () {
if (!Form.of(context)!.validate()) {
return;
}
Navigator.of(context).pop(controller.text); FileSelector(
}, label: "Location",
child: const Text('Save') 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 { enum ContextualOption {
openExplorer, openExplorer,
rename, modify,
enableMemoryFix,
disableMemoryFix,
delete; 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 { String get name {
return this == ContextualOption.openExplorer ? "Open in explorer" return this == ContextualOption.openExplorer ? "Open in explorer"
: this == ContextualOption.rename ? "Rename" : this == ContextualOption.modify ? "Modify"
: this == ContextualOption.enableMemoryFix ? "Enable memory leak fix"
: this == ContextualOption.disableMemoryFix ? "Disable memory leak fix"
: "Delete"; : "Delete";
} }
} }

View File

@@ -12,10 +12,10 @@ class HostInput extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Tooltip( return Tooltip(
message: "The hostname of the lawin server", message: "The hostname of the backend server",
child: Obx(() => SmartInput( child: Obx(() => SmartInput(
label: "Host", label: "Host",
placeholder: "Type the lawin server's hostname", placeholder: "Type the backend server's hostname",
controller: _serverController.host, controller: _serverController.host,
enabled: _serverController.type.value == ServerType.remote enabled: _serverController.type.value == ServerType.remote
)) ))

View File

@@ -13,10 +13,10 @@ class PortInput extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Tooltip( return Tooltip(
message: "The port of the lawin server", message: "The port of the backend server",
child: Obx(() => SmartInput( child: Obx(() => SmartInput(
label: "Port", label: "Port",
placeholder: "Type the lawin server's port", placeholder: "Type the backend server's port",
controller: _serverController.port, controller: _serverController.port,
enabled: _serverController.type.value != ServerType.embedded enabled: _serverController.type.value != ServerType.embedded
)) ))

View File

@@ -23,7 +23,10 @@ class _ServerButtonState extends State<ServerButton> {
child: Obx(() => Tooltip( child: Obx(() => Tooltip(
message: _helpMessage, message: _helpMessage,
child: Button( child: Button(
onPressed: () async => _serverController.changeStateInteractive(false), onPressed: () async => _serverController.start(
required: false,
askPortKill: true
),
child: Text(_buttonText())), child: Text(_buttonText())),
)), )),
), ),
@@ -46,18 +49,18 @@ class _ServerButtonState extends State<ServerButton> {
switch(_serverController.type.value){ switch(_serverController.type.value){
case ServerType.embedded: case ServerType.embedded:
if (_serverController.started.value) { 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: case ServerType.remote:
if (_serverController.started.value) { if (_serverController.started.value) {
return "Stop the reverse proxy currently running"; 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: case ServerType.local:
return "Check if a local lawin server is running"; return "Check if a local backend server is running";
} }
} }
} }

View File

@@ -11,7 +11,7 @@ class ServerTypeSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Tooltip( return Tooltip(
message: "Determines the type of lawin server to use", message: "Determines the type of backend server to use",
child: InfoLabel( child: InfoLabel(
label: "Type", label: "Type",
child: SizedBox( child: SizedBox(
@@ -35,7 +35,10 @@ class ServerTypeSelector extends StatelessWidget {
child: Text(type.name) child: Text(type.name)
) )
), ),
onPressed: () => _serverController.type(type) onPressed: () async {
await _serverController.stop();
_serverController.type(type);
}
); );
} }

View 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);
}
}

View 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);
}

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Launcher for project reboot description: Launcher for project reboot
version: "4.4.0" version: "5.4.0"
publish_to: 'none' publish_to: 'none'
@@ -13,7 +13,7 @@ dependencies:
bitsdojo_window: bitsdojo_window:
path: ./dependencies/bitsdojo_window-0.1.5 path: ./dependencies/bitsdojo_window-0.1.5
fluent_ui: ^4.0.2 fluent_ui: ^4.0.3+1
bitsdojo_window_windows: ^0.1.5 bitsdojo_window_windows: ^0.1.5
system_theme: ^2.0.0 system_theme: ^2.0.0
http: ^0.13.5 http: ^0.13.5
@@ -37,6 +37,11 @@ dependencies:
clipboard: ^0.1.3 clipboard: ^0.1.3
sync: ^0.3.0 sync: ^0.3.0
ini: ^2.1.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: dependency_overrides:
win32: ^3.0.0 win32: ^3.0.0
@@ -47,7 +52,6 @@ dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^2.0.1
msix: ^3.6.3 msix: ^3.6.3
hex: ^0.2.0
flutter: flutter:
uses-material-design: true uses-material-design: true
@@ -55,12 +59,15 @@ flutter:
- assets/icons/ - assets/icons/
- assets/binaries/ - assets/binaries/
- assets/images/ - assets/images/
- assets/profiles/
- assets/responses/
- assets/config/
msix_config: msix_config:
display_name: Reboot Launcher display_name: Reboot Launcher
publisher_display_name: Auties00 publisher_display_name: Auties00
identity_name: 31868Auties00.RebootLauncher identity_name: 31868Auties00.RebootLauncher
msix_version: 4.4.0.0 msix_version: 5.4.0.0
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
logo_path: ./assets/icons/reboot.ico logo_path: ./assets/icons/reboot.ico
architecture: x64 architecture: x64

View File

@@ -40,19 +40,19 @@ Pretty much don't touch this except you really know what you are doing
### Host ### 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 ### 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 ### 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 ### 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 # Info

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h> #include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <dart_vlc/dart_vlc_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
#include <system_theme/system_theme_plugin.h> #include <system_theme/system_theme_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@@ -15,6 +16,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
BitsdojoWindowPluginRegisterWithRegistrar( BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
DartVlcPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DartVlcPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar( ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SystemThemePluginRegisterWithRegistrar( SystemThemePluginRegisterWithRegistrar(

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows bitsdojo_window_windows
dart_vlc
screen_retriever screen_retriever
system_theme system_theme
url_launcher_windows url_launcher_windows