diff --git a/data/ui/xbox/platform_web_white.png b/data/ui/xbox/platform_web_white.png index d1e087d1..6a30a2f3 100644 Binary files a/data/ui/xbox/platform_web_white.png and b/data/ui/xbox/platform_web_white.png differ diff --git a/src/Http.hx b/src/Http.hx index dea20cd3..075ca150 100644 --- a/src/Http.hx +++ b/src/Http.hx @@ -26,6 +26,8 @@ class Http { threadPool = new sys.thread.FixedThreadPool(2); threadPool.run(() -> threadLoop()); threadPool.run(() -> threadLoop()); + threadPool.run(() -> threadLoop()); + threadPool.run(() -> threadLoop()); #end } diff --git a/src/MPCustoms.hx b/src/MPCustoms.hx new file mode 100644 index 00000000..0ccf1407 --- /dev/null +++ b/src/MPCustoms.hx @@ -0,0 +1,68 @@ +import src.MissionList; +import gui.MessageBoxOkDlg; +import haxe.zip.Reader; +import haxe.io.BytesInput; +import haxe.Json; +import src.Http; +import src.Console; +import src.MarbleGame; +import src.ResourceLoader; + +typedef MPCustomEntry = { + artist:String, + description:String, + path:String, + title:String +}; + +class MPCustoms { + public static var missionList:Array = []; + + static var _requestSent = false; + + public static function loadMissionList() { + if (missionList.length == 0 && !_requestSent) { + _requestSent = true; + Http.get("https://marbleblastultra.randomityguy.me/data/ultraCustom.json", (b) -> { + var misList = Json.parse(b.toString()); + missionList = misList; + Console.log('Loaded ${misList.length} custom missions.'); + _requestSent = false; + }, (e) -> { + Console.log('Error getting custom list from marbleland.'); + _requestSent = false; + }); + } + } + + public static function download(mission:MPCustomEntry, onFinish:() -> Void, onFail:() -> Void) { + var lastSlashIdx = mission.path.lastIndexOf('/'); + var dlPath = "https://marbleblastultra.randomityguy.me/" + mission.path.substr(0, lastSlashIdx) + ".zip"; + Http.get(dlPath, (zipData) -> { + var reader = new Reader(new BytesInput(zipData)); + var entries:Array = null; + try { + entries = [for (x in reader.read()) x]; + } catch (e) {} + ResourceLoader.loadZip(entries, 'missions/mpcustom/'); + if (entries != null) { + onFinish(); + } else { + MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("Failed to download mission")); + onFail(); + } + }, (e) -> { + MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("Failed to download mission")); + onFail(); + }); + } + + public static function play(mission:MPCustomEntry, onFinish:() -> Void, onFail:() -> Void) { + download(mission, () -> { + var f = ResourceLoader.getFileEntry(mission.path); + var mis = MissionList.parseMisHeader(f.entry.getBytes().toString(), mission.path); + MarbleGame.instance.playMission(mis, true); + onFinish(); + }, onFail); + } +} diff --git a/src/Main.hx b/src/Main.hx index 3ae89905..4a208013 100644 --- a/src/Main.hx +++ b/src/Main.hx @@ -94,6 +94,7 @@ class Main extends hxd.App { haxe.MainLoop.add(() -> Http.loop()); Settings.init(); Gamepad.init(); + MPCustoms.loadMissionList(); ResourceLoader.init(s2d, () -> { AudioManager.init(); AudioManager.playShell(); diff --git a/src/MissionList.hx b/src/MissionList.hx index 1fb8789f..03ceda7d 100644 --- a/src/MissionList.hx +++ b/src/MissionList.hx @@ -78,6 +78,8 @@ class MissionList { ultraMissions.set("intermediate", parseDifficulty("ultra", "missions", "intermediate", 1)); ultraMissions.set("advanced", parseDifficulty("ultra", "missions", "advanced", 2)); ultraMissions.set("multiplayer", parseDifficulty("ultra", "missions", "multiplayer", 3)); + // var mpCustoms = parseDifficulty("ultra", "missions", "mpcustom", 3); + // ultraMissions["multiplayer"] = ultraMissions["multiplayer"].concat(mpCustoms); @:privateAccess ultraMissions["beginner"][ultraMissions["beginner"].length - 1].next = ultraMissions["intermediate"][0]; @:privateAccess ultraMissions["intermediate"][ultraMissions["intermediate"].length - 1].next = ultraMissions["advanced"][0]; @@ -91,4 +93,16 @@ class MissionList { _build = true; } + + public static function parseMisHeader(conts:String, path:String) { + var misParser = new MisParser(conts); + var mInfo = misParser.parseMissionInfo(); + var mission = Mission.fromMissionInfo(path, mInfo); + mission.game = "ultra"; + // do egg thing + if (StringTools.contains(conts.toLowerCase(), 'datablock = "easteregg"')) { // Ew + mission.hasEgg = true; + } + return mission; + } } diff --git a/src/ResourceLoader.hx b/src/ResourceLoader.hx index cecc0d04..0e114838 100644 --- a/src/ResourceLoader.hx +++ b/src/ResourceLoader.hx @@ -1,5 +1,6 @@ package src; +import haxe.zip.Uncompress; import hxd.res.Any; import hxd.fs.BytesFileSystem.BytesFileEntry; #if (js || android) @@ -590,20 +591,21 @@ class ResourceLoader { return names; } - public static function loadZip(entries:Array, game:String) { + public static function loadZip(entries:Array, prefix:String) { zipFilesystem.clear(); // We are only allowed to load one zip for (entry in entries) { - var fname = entry.fileName.toLowerCase(); + var fname = prefix + entry.fileName.toLowerCase(); #if sys fname = "data/" + fname; #end - if (game == 'gold') - fname = StringTools.replace(fname, 'interiors/', 'interiors_mbg/'); fname = StringTools.replace(fname, "lbinteriors", "interiors"); // Normalize if (exists(fname)) continue; Console.log("Loaded zip entry: " + fname); - var zfe = new BytesFileEntry(fname, entry.data); + + var zdata = entry.data; + + var zfe = new BytesFileEntry(fname, zdata); zipFilesystem.set(fname, zfe); } } diff --git a/src/gui/MainMenuGui.hx b/src/gui/MainMenuGui.hx index 5fd144dc..756766ab 100644 --- a/src/gui/MainMenuGui.hx +++ b/src/gui/MainMenuGui.hx @@ -72,7 +72,11 @@ class MainMenuGui extends GuiImage { cast(this.parent, Canvas).setContent(new DifficultySelectGui()); }); btnList.addButton(0, "Multiplayer Game", (sender) -> { - cast(this.parent, Canvas).setContent(new MultiplayerGui()); + if (MPCustoms.missionList.length == 0) { + cast(this.parent, Canvas).pushDialog(new MessageBoxOkDlg("Custom levels not loaded yet, please wait.")); + MPCustoms.loadMissionList(); + } else + cast(this.parent, Canvas).setContent(new MultiplayerGui()); }); // btnList.addButton(2, "Leaderboards", (e) -> {}, 20); btnList.addButton(2, "Achievements", (e) -> { diff --git a/src/gui/MultiplayerLevelSelectGui.hx b/src/gui/MultiplayerLevelSelectGui.hx index 9b2d24ae..845def91 100644 --- a/src/gui/MultiplayerLevelSelectGui.hx +++ b/src/gui/MultiplayerLevelSelectGui.hx @@ -20,12 +20,19 @@ class MultiplayerLevelSelectGui extends GuiImage { static var setLevelFn:Int->Void; static var playSelectedLevel:Int->Void; + static var setLevelStr:String->Void; var playerList:GuiMLTextListCtrl; + var customList:GuiTextListCtrl; var updatePlayerCountFn:(Int, Int, Int, Int) -> Void; var innerCtrl:GuiControl; var inviteVisibility:Bool = true; + static var custSelected:Bool = false; + static var custPath:String; + + var showingCustoms = false; + public function new(isHost:Bool) { var res = ResourceLoader.getImage("data/ui/game/CloudBG.jpg").resource.toTile(); super(res); @@ -150,6 +157,8 @@ class MultiplayerLevelSelectGui extends GuiImage { playerWnd.extent = new Vector(640, 480); innerCtrl.addChild(playerWnd); + custSelected = false; + var playerListArr = []; if (Net.isHost) { playerListArr.push({ @@ -212,6 +221,31 @@ class MultiplayerLevelSelectGui extends GuiImage { playerList.onSelectedFunc = (sel) -> {} playerWnd.addChild(playerList); + var customListScroll = new GuiConsoleScrollCtrl(ResourceLoader.getResource("data/ui/common/osxscroll.png", ResourceLoader.getImage, this.imageResources) + .toTile()); + customListScroll.position = new Vector(25, 22); + customListScroll.extent = new Vector(590, 330); + + customList = new GuiTextListCtrl(arial14, MPCustoms.missionList.map(mission -> { + return mission.title; + })); + var custSelectedIdx = 0; + customList.selectedColor = 0xF29515; + customList.selectedFillColor = 0xEBEBEB; + customList.position = new Vector(0, 0); + customList.extent = new Vector(550, 2880); + customList.scrollable = true; + customList.onSelectedFunc = (idx) -> { + NetCommands.setLobbyCustLevelName(MPCustoms.missionList[idx].title); + custSelected = true; + custSelectedIdx = idx; + custPath = MPCustoms.missionList[idx].path; + updateLobbyNames(); + } + customListScroll.addChild(customList); + customListScroll.setScrollMax(customList.calculateFullHeight()); + // playerWnd.addChild(customList); + var bottomBar = new GuiControl(); bottomBar.position = new Vector(0, 590); bottomBar.extent = new Vector(640, 200); @@ -236,6 +270,25 @@ class MultiplayerLevelSelectGui extends GuiImage { bottomBar.addChild(backButton); if (Net.isHost) { + var customsButton = new GuiXboxButton("Customs", 200); + customsButton.position = new Vector(560, 0); + customsButton.vertSizing = Bottom; + customsButton.horizSizing = Right; + customsButton.gamepadAccelerator = ["X"]; + customsButton.pressedAction = (e) -> { + showingCustoms = !showingCustoms; + if (showingCustoms) { + playerWnd.addChild(customListScroll); + playerWnd.removeChild(playerList); + } else { + playerWnd.removeChild(customListScroll); + playerWnd.addChild(playerList); + updateLobbyNames(); + } + MarbleGame.canvas.render(MarbleGame.canvas.scene2d); + } + bottomBar.addChild(customsButton); + var inviteButton = new GuiXboxButton("Invite Visibility", 220); inviteButton.position = new Vector(750, 0); inviteButton.vertSizing = Bottom; @@ -259,8 +312,12 @@ class MultiplayerLevelSelectGui extends GuiImage { bottomBar.addChild(nextButton); playSelectedLevel = (index:Int) -> { - curMission = difficultyMissions[index]; - MarbleGame.instance.playMission(curMission, true); + if (custSelected) { + NetCommands.playCustomLevel(MPCustoms.missionList[custSelectedIdx].path); + } else { + curMission = difficultyMissions[index]; + MarbleGame.instance.playMission(curMission, true); + } } var levelWnd = new GuiImage(ResourceLoader.getResource("data/ui/xbox/levelPreviewWindow.png", ResourceLoader.getImage, this.imageResources).toTile()); @@ -289,6 +346,7 @@ class MultiplayerLevelSelectGui extends GuiImage { function setLevel(idx:Int) { // if (lock) // return false; + custSelected = false; levelSelectOpts.currentOption = idx; this.bmp.visible = true; loadAnim.anim.visible = true; @@ -319,12 +377,12 @@ class MultiplayerLevelSelectGui extends GuiImage { updatePlayerCountFn = (pub:Int, priv:Int, publicTotal:Int, privateTotal:Int) -> { if (inviteVisibility) levelInfoLeft.text.text = '

Host: ${hostName}

' - + '

Level: ${mis.title}

' + + '

Level: ${levelSelectOpts.optionText.text.text}

' + '

Public Slots: ${pub}/${publicTotal}, Private Slots: ${priv}/${privateTotal}, Invite Code: ${Net.serverInfo.inviteCode}

'; else levelInfoLeft.text.text = '

Host: ${hostName}

' - + '

Level: ${mis.title}

' + + '

Level: ${levelSelectOpts.optionText.text.text}

' + '

Public Slots: ${pub}/${publicTotal}, Private Slots: ${priv}/${privateTotal}

'; } var pubCount = 1; // 1 for host @@ -341,7 +399,8 @@ class MultiplayerLevelSelectGui extends GuiImage { } if (Net.isClient) { updatePlayerCountFn = (pub:Int, priv:Int, publicTotal:Int, privateTotal:Int) -> { - levelInfoLeft.text.text = '

Host: ${hostName}

' + '

Level: ${mis.title}

'; + levelInfoLeft.text.text = '

Host: ${hostName}

' + + '

Level: ${levelSelectOpts.optionText.text.text}

'; } updatePlayerCountFn(0, 0, 0, 0); } @@ -363,6 +422,11 @@ class MultiplayerLevelSelectGui extends GuiImage { levelSelectOpts.setCurrentOption(idx); }; + setLevelStr = (str) -> { + levelSelectOpts.optionText.text.text = str; + updateLobbyNames(); + } + levelSelectOpts.setCurrentOption(currentSelectionStatic); setLevel(currentSelectionStatic); innerCtrl.addChild(levelSelectOpts); @@ -416,9 +480,10 @@ class MultiplayerLevelSelectGui extends GuiImage { } } - playerList.setTexts(playerListArr.map(player -> { - return '${player.name}'; - })); + if (!showingCustoms) + playerList.setTexts(playerListArr.map(player -> { + return '${player.name}'; + })); var pubCount = 1; // Self var privCount = 0; diff --git a/src/gui/MultiplayerLoadingGui.hx b/src/gui/MultiplayerLoadingGui.hx index 2676c7f8..75f4b6e2 100644 --- a/src/gui/MultiplayerLoadingGui.hx +++ b/src/gui/MultiplayerLoadingGui.hx @@ -16,7 +16,7 @@ class MultiplayerLoadingGui extends GuiImage { var innerCtrl:GuiControl; var backButton:GuiXboxButton; - public function new(initialStatus:String) { + public function new(initialStatus:String, showCancel = true) { var res = ResourceLoader.getImage("data/ui/game/CloudBG.jpg").resource.toTile(); super(res); this.position = new Vector(); @@ -89,17 +89,19 @@ class MultiplayerLoadingGui extends GuiImage { bottomBar.vertSizing = Bottom; innerCtrl.addChild(bottomBar); - backButton = new GuiXboxButton("Cancel", 160); - backButton.position = new Vector(960, 0); - backButton.vertSizing = Bottom; - backButton.horizSizing = Right; - backButton.gamepadAccelerator = ["A"]; - backButton.accelerators = [hxd.Key.ENTER]; - backButton.pressedAction = (e) -> { - Net.disconnect(); - MarbleGame.canvas.setContent(new MultiplayerGui()); - }; - bottomBar.addChild(backButton); + if (showCancel) { + backButton = new GuiXboxButton("Cancel", 160); + backButton.position = new Vector(960, 0); + backButton.vertSizing = Bottom; + backButton.horizSizing = Right; + backButton.gamepadAccelerator = ["A"]; + backButton.accelerators = [hxd.Key.ENTER]; + backButton.pressedAction = (e) -> { + Net.disconnect(); + MarbleGame.canvas.setContent(new MultiplayerGui()); + }; + bottomBar.addChild(backButton); + } } public function setLoadingStatus(str:String) { diff --git a/src/net/Net.hx b/src/net/Net.hx index 9b93c388..d8f5333b 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -564,7 +564,10 @@ class Net { NetCommands.setLobbyLevelIndexClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic); if (serverInfo.state == "PLAYING") { // We initiated the game, directly add in the marble - NetCommands.playLevelMidJoinClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic); + if (MultiplayerLevelSelectGui.custSelected) { + NetCommands.playCustomLevelMidJoinClient(conn, MultiplayerLevelSelectGui.custPath); + } else + NetCommands.playLevelMidJoinClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic); MarbleGame.instance.world.addJoiningClient(conn, () -> {}); var playerInfoBytes = sendPlayerInfosBytes(); for (dc => cc in clients) { diff --git a/src/net/NetCommands.hx b/src/net/NetCommands.hx index 405a2128..849ee071 100644 --- a/src/net/NetCommands.hx +++ b/src/net/NetCommands.hx @@ -1,5 +1,6 @@ package net; +import gui.MultiplayerGui; import net.ClientConnection.NetPlatform; import gui.EndGameGui; import modes.HuntMode; @@ -21,6 +22,12 @@ class NetCommands { } } + @:rpc(server) public static function setLobbyCustLevelName(str:String) { + if (MultiplayerLevelSelectGui.setLevelFn != null) { + MultiplayerLevelSelectGui.setLevelStr(str); + } + } + @:rpc(server) public static function playLevel(levelIndex:Int) { MultiplayerLevelSelectGui.playSelectedLevel(levelIndex); if (Net.isHost) { @@ -29,6 +36,19 @@ class NetCommands { } } + @:rpc(server) public static function playCustomLevel(levelPath:String) { + var levelEntry = MPCustoms.missionList.filter(x -> x.path == levelPath)[0]; + MarbleGame.canvas.setContent(new MultiplayerLoadingGui("Downloading", false)); + MPCustoms.play(levelEntry, () -> {}, () -> { + MarbleGame.canvas.setContent(new MultiplayerGui()); + Net.disconnect(); // disconnect from the server + }); + if (Net.isHost) { + Net.serverInfo.state = "WAITING"; + MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the wait state + } + } + @:rpc(server) public static function playLevelMidJoin(index:Int) { if (Net.isClient) { var difficultyMissions = MissionList.missionList['ultra']["multiplayer"]; @@ -37,6 +57,12 @@ class NetCommands { } } + @:rpc(server) public static function playCustomLevelMidJoin(path:String) { + if (Net.isClient) { + playCustomLevel(path); + } + } + @:rpc(server) public static function enterLobby() { if (Net.isClient) { MarbleGame.canvas.setContent(new MultiplayerLevelSelectGui(false)); @@ -77,7 +103,10 @@ class NetCommands { } if (allReady && Net.lobbyHostReady) { - NetCommands.playLevel(MultiplayerLevelSelectGui.currentSelectionStatic); + if (MultiplayerLevelSelectGui.custSelected) { + NetCommands.playCustomLevel(MultiplayerLevelSelectGui.custPath); + } else + NetCommands.playLevel(MultiplayerLevelSelectGui.currentSelectionStatic); } } } diff --git a/src/net/NetPacket.hx b/src/net/NetPacket.hx index fe85f7e5..6ee74c82 100644 --- a/src/net/NetPacket.hx +++ b/src/net/NetPacket.hx @@ -173,13 +173,13 @@ class PowerupPickupPacket implements NetPacket { public inline function deserialize(b:InputBitStream) { clientId = b.readByte(); serverTicks = b.readUInt16(); - powerupItemId = b.readInt(9); + powerupItemId = b.readInt(10); } public inline function serialize(b:OutputBitStream) { b.writeByte(clientId); b.writeUInt16(serverTicks); - b.writeInt(powerupItemId, 9); + b.writeInt(powerupItemId, 10); } } @@ -194,14 +194,14 @@ class GemSpawnPacket implements NetPacket { public function serialize(b:OutputBitStream) { b.writeInt(gemIds.length, 5); for (gemId in gemIds) { - b.writeInt(gemId, 10); + b.writeInt(gemId, 11); } } public function deserialize(b:InputBitStream) { var count = b.readInt(5); for (i in 0...count) { - gemIds.push(b.readInt(10)); + gemIds.push(b.readInt(11)); } } } @@ -218,14 +218,14 @@ class GemPickupPacket implements NetPacket { public inline function deserialize(b:InputBitStream) { clientId = b.readByte(); serverTicks = b.readUInt16(); - gemId = b.readInt(10); + gemId = b.readInt(11); scoreIncr = b.readInt(4); } public inline function serialize(b:OutputBitStream) { b.writeByte(clientId); b.writeUInt16(serverTicks); - b.writeInt(gemId, 10); + b.writeInt(gemId, 11); b.writeInt(scoreIncr, 4); } }