add support for customs in MP

This commit is contained in:
RandomityGuy 2024-06-02 22:27:26 +05:30
parent 83f72c4dfe
commit b3ba40ed94
12 changed files with 224 additions and 34 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 450 B

View file

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

68
src/MPCustoms.hx Normal file
View file

@ -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<MPCustomEntry> = [];
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<haxe.zip.Entry> = 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);
}
}

View file

@ -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();

View file

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

View file

@ -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<haxe.zip.Entry>, game:String) {
public static function loadZip(entries:Array<haxe.zip.Entry>, 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);
}
}

View file

@ -72,6 +72,10 @@ class MainMenuGui extends GuiImage {
cast(this.parent, Canvas).setContent(new DifficultySelectGui());
});
btnList.addButton(0, "Multiplayer Game", (sender) -> {
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);

View file

@ -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,9 +312,13 @@ class MultiplayerLevelSelectGui extends GuiImage {
bottomBar.addChild(nextButton);
playSelectedLevel = (index:Int) -> {
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());
levelWnd.position = new Vector(555, 469);
@ -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 = '<p><font face="arial14">Host: ${hostName}</font></p>'
+ '<p><font face="arial14">Level: ${mis.title}</font></p>'
+ '<p><font face="arial14">Level: ${levelSelectOpts.optionText.text.text}</font></p>'
+
'<p><font face="arial12">Public Slots: ${pub}/${publicTotal}, Private Slots: ${priv}/${privateTotal}, Invite Code: ${Net.serverInfo.inviteCode}</font></p>';
else
levelInfoLeft.text.text = '<p><font face="arial14">Host: ${hostName}</font></p>'
+ '<p><font face="arial14">Level: ${mis.title}</font></p>'
+ '<p><font face="arial14">Level: ${levelSelectOpts.optionText.text.text}</font></p>'
+ '<p><font face="arial12">Public Slots: ${pub}/${publicTotal}, Private Slots: ${priv}/${privateTotal}</font></p>';
}
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 = '<p><font face="arial14">Host: ${hostName}</font></p>' + '<p><font face="arial14">Level: ${mis.title}</font></p>';
levelInfoLeft.text.text = '<p><font face="arial14">Host: ${hostName}</font></p>'
+ '<p><font face="arial14">Level: ${levelSelectOpts.optionText.text.text}</font></p>';
}
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,6 +480,7 @@ class MultiplayerLevelSelectGui extends GuiImage {
}
}
if (!showingCustoms)
playerList.setTexts(playerListArr.map(player -> {
return '<img src="${player.state ? "ready" : "notready"}"></img><img src="${platformToString(player.platform)}"></img>${player.name}';
}));

View file

@ -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,6 +89,7 @@ class MultiplayerLoadingGui extends GuiImage {
bottomBar.vertSizing = Bottom;
innerCtrl.addChild(bottomBar);
if (showCancel) {
backButton = new GuiXboxButton("Cancel", 160);
backButton.position = new Vector(960, 0);
backButton.vertSizing = Bottom;
@ -101,6 +102,7 @@ class MultiplayerLoadingGui extends GuiImage {
};
bottomBar.addChild(backButton);
}
}
public function setLoadingStatus(str:String) {
loadText.text.text = str;

View file

@ -564,6 +564,9 @@ class Net {
NetCommands.setLobbyLevelIndexClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic);
if (serverInfo.state == "PLAYING") { // We initiated the game, directly add in the marble
if (MultiplayerLevelSelectGui.custSelected) {
NetCommands.playCustomLevelMidJoinClient(conn, MultiplayerLevelSelectGui.custPath);
} else
NetCommands.playLevelMidJoinClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic);
MarbleGame.instance.world.addJoiningClient(conn, () -> {});
var playerInfoBytes = sendPlayerInfosBytes();

View file

@ -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,6 +103,9 @@ class NetCommands {
}
if (allReady && Net.lobbyHostReady) {
if (MultiplayerLevelSelectGui.custSelected) {
NetCommands.playCustomLevel(MultiplayerLevelSelectGui.custPath);
} else
NetCommands.playLevel(MultiplayerLevelSelectGui.currentSelectionStatic);
}
}

View file

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