it is done

This commit is contained in:
Anuken
2019-12-25 01:39:38 -05:00
parent 5b21873f3c
commit 514d4817c8
488 changed files with 4572 additions and 4574 deletions

View File

@@ -0,0 +1,319 @@
package mindustry.desktop;
import arc.*;
import club.minnced.discord.rpc.*;
import com.codedisaster.steamworks.*;
import arc.Files.*;
import io.anuke.arc.backends.sdl.*;
import io.anuke.arc.backends.sdl.jni.*;
import arc.struct.*;
import arc.files.*;
import arc.func.*;
import arc.math.*;
import arc.util.*;
import arc.util.serialization.*;
import mindustry.*;
import mindustry.core.GameState.*;
import mindustry.core.*;
import mindustry.desktop.steam.*;
import mindustry.game.EventType.*;
import mindustry.net.*;
import mindustry.net.Net.*;
import mindustry.type.*;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
import static mindustry.Vars.*;
public class DesktopLauncher extends ClientLauncher{
public final static String discordID = "610508934456934412";
boolean useDiscord = OS.is64Bit, loadError = false;
Throwable steamError;
static{
if(!Charset.forName("US-ASCII").newEncoder().canEncode(System.getProperty("user.name", ""))){
System.setProperty("com.codedisaster.steamworks.SharedLibraryExtractPath", new File("").getAbsolutePath());
}
}
public static void main(String[] arg){
try{
Vars.loadLogger();
new SdlApplication(new DesktopLauncher(arg), new SdlConfig(){{
title = "Mindustry";
maximized = true;
depth = 0;
stencil = 0;
width = 900;
height = 700;
setWindowIcon(FileType.internal, "icons/icon_64.png");
}});
}catch(Throwable e){
handleCrash(e);
}
}
public DesktopLauncher(String[] args){
Version.init();
boolean useSteam = Version.modifier.contains("steam");
testMobile = Array.with(args).contains("-testMobile");
if(useDiscord){
try{
DiscordEventHandlers handlers = new DiscordEventHandlers();
DiscordRPC.INSTANCE.Discord_Initialize(discordID, handlers, true, "1127400");
Log.info("Initialized Discord rich presence.");
Runtime.getRuntime().addShutdownHook(new Thread(DiscordRPC.INSTANCE::Discord_Shutdown));
}catch(Throwable t){
useDiscord = false;
Log.err("Failed to initialize discord.", t);
}
}
if(useSteam){
//delete leftover dlls
Fi file = new Fi(".");
for(Fi other : file.parent().list()){
if(other.name().contains("steam") && (other.extension().equals("dll") || other.extension().equals("so") || other.extension().equals("dylib"))){
other.delete();
}
}
Events.on(ClientLoadEvent.class, event -> {
if(steamError != null){
Core.app.post(() -> Core.app.post(() -> Core.app.post(() -> {
ui.showErrorMessage(Core.bundle.format("steam.error", (steamError.getMessage() == null) ? steamError.getClass().getSimpleName() : steamError.getClass().getSimpleName() + ": " + steamError.getMessage()));
})));
}
});
try{
SteamAPI.loadLibraries();
if(!SteamAPI.init()){
loadError = true;
Log.err("Steam client not running.");
}else{
initSteam(args);
Vars.steam = true;
}
if(SteamAPI.restartAppIfNecessary(SVars.steamID)){
System.exit(0);
}
}catch(NullPointerException ignored){
steam = false;
Log.info("Running in offline mode.");
}catch(Throwable e){
steam = false;
Log.err("Failed to load Steam native libraries.");
logSteamError(e);
}
}
}
void logSteamError(Throwable e){
steamError = e;
loadError = true;
Log.err(e);
try(OutputStream s = new FileOutputStream(new File("steam-error-log-" + System.nanoTime() + ".txt"))){
String log = Strings.parseException(e, true);
s.write(log.getBytes());
}catch(Exception e2){
Log.err(e2);
}
}
void initSteam(String[] args){
SVars.net = new SNet(new ArcNetProvider());
SVars.stats = new SStats();
SVars.workshop = new SWorkshop();
SVars.user = new SUser();
boolean[] isShutdown = {false};
Events.on(ClientLoadEvent.class, event -> {
player.name = SVars.net.friends.getPersonaName();
Core.settings.defaults("name", SVars.net.friends.getPersonaName());
Core.settings.put("name", player.name);
Core.settings.save();
//update callbacks
Core.app.addListener(new ApplicationListener(){
@Override
public void update(){
if(SteamAPI.isSteamRunning()){
SteamAPI.runCallbacks();
}
}
});
Core.app.post(() -> {
if(args.length >= 2 && args[0].equals("+connect_lobby")){
try{
long id = Long.parseLong(args[1]);
ui.join.connect("steam:" + id, port);
}catch(Exception e){
Log.err("Failed to parse steam lobby ID: {0}", e.getMessage());
e.printStackTrace();
}
}
});
});
Events.on(DisposeEvent.class, event -> {
SteamAPI.shutdown();
isShutdown[0] = true;
});
//steam shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if(!isShutdown[0]){
SteamAPI.shutdown();
}
}));
}
static void handleCrash(Throwable e){
Cons<Runnable> dialog = Runnable::run;
boolean badGPU = false;
if(e.getMessage() != null && (e.getMessage().contains("Couldn't create window") || e.getMessage().contains("OpenGL 2.0 or higher") || e.getMessage().toLowerCase().contains("pixel format"))){
dialog.get(() -> message(
e.getMessage().contains("Couldn't create window") ? "A graphics initialization error has occured! Try to update your graphics drivers:\n" + e.getMessage() :
"Your graphics card does not support OpenGL 2.0!\n" +
"Try to update your graphics drivers.\n\n" +
"(If that doesn't work, your computer just doesn't support Mindustry.)"));
badGPU = true;
}
boolean fbgp = badGPU;
CrashSender.send(e, file -> {
Throwable fc = Strings.getFinalCause(e);
if(!fbgp){
dialog.get(() -> message("A crash has occured. It has been saved in:\n" + file.getAbsolutePath() + "\n" + fc.getClass().getSimpleName().replace("Exception", "") + (fc.getMessage() == null ? "" : ":\n" + fc.getMessage())));
}
});
}
@Override
public Array<Fi> getWorkshopContent(Class<? extends Publishable> type){
return !steam ? super.getWorkshopContent(type) : SVars.workshop.getWorkshopFiles(type);
}
@Override
public void viewListing(Publishable pub){
SVars.workshop.viewListing(pub);
}
@Override
public void viewListingID(String id){
SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + id);
}
@Override
public NetProvider getNet(){
return steam ? SVars.net : new ArcNetProvider();
}
@Override
public void openWorkshop(){
SVars.net.friends.activateGameOverlayToWebPage("https://steamcommunity.com/app/1127400/workshop/");
}
@Override
public void publish(Publishable pub){
SVars.workshop.publish(pub);
}
@Override
public void inviteFriends(){
SVars.net.showFriendInvites();
}
@Override
public void updateLobby(){
SVars.net.updateLobby();
}
@Override
public void updateRPC(){
if(!useDiscord) return;
DiscordRichPresence presence = new DiscordRichPresence();
if(!state.is(State.menu)){
String map = world.getMap() == null ? "Unknown Map" : world.isZone() ? world.getZone().localizedName : Strings.capitalize(world.getMap().name());
String mode = state.rules.pvp ? "PvP" : state.rules.attackMode ? "Attack" : "Survival";
String players = net.active() && playerGroup.size() > 1 ? " | " + playerGroup.size() + " Players" : "";
presence.state = mode + players;
if(!state.rules.waves){
presence.details = map;
}else{
presence.details = map + " | Wave " + state.wave;
presence.largeImageText = "Wave " + state.wave;
}
}else{
if(ui.editor != null && ui.editor.isShown()){
presence.state = "In Editor";
}else if(ui.deploy != null && ui.deploy.isShown()){
presence.state = "In Launch Selection";
}else{
presence.state = "In Menu";
}
}
presence.largeImageKey = "logo";
DiscordRPC.INSTANCE.Discord_UpdatePresence(presence);
}
@Override
public String getUUID(){
if(steam){
try{
byte[] result = new byte[8];
new RandomXS128(SVars.user.user.getSteamID().getAccountID()).nextBytes(result);
return new String(Base64Coder.encode(result));
}catch(Exception e){
e.printStackTrace();
}
}
try{
Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
NetworkInterface out;
for(out = e.nextElement(); (out.getHardwareAddress() == null || !validAddress(out.getHardwareAddress())) && e.hasMoreElements(); out = e.nextElement());
byte[] bytes = out.getHardwareAddress();
byte[] result = new byte[8];
System.arraycopy(bytes, 0, result, 0, bytes.length);
String str = new String(Base64Coder.encode(result));
if(str.equals("AAAAAAAAAOA=") || str.equals("AAAAAAAAAAA=")) throw new RuntimeException("Bad UUID.");
return str;
}catch(Exception e){
return super.getUUID();
}
}
private static void message(String message){
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MESSAGEBOX_ERROR, "oh no", message);
}
private boolean validAddress(byte[] bytes){
if(bytes == null) return false;
byte[] result = new byte[8];
System.arraycopy(bytes, 0, result, 0, bytes.length);
return !new String(Base64Coder.encode(result)).equals("AAAAAAAAAOA=") && !new String(Base64Coder.encode(result)).equals("AAAAAAAAAAA=");
}
}

View File

@@ -0,0 +1,87 @@
package mindustry.desktop.steam;
public enum SAchievement{
completeTutorial,
kill1kEnemies(SStat.unitsDestroyed, 1000),
kill100kEnemies(SStat.unitsDestroyed, 1000 * 100),
launch10kItems(SStat.itemsLaunched, 1000 * 10),
launch1milItems(SStat.itemsLaunched, 1000 * 1000),
win10Attack(SStat.attacksWon, 10),
win10PvP(SStat.pvpsWon, 10),
defeatAttack5Waves,
launch30Times(SStat.timesLaunched, 30),
survive100Waves(SStat.maxWavesSurvived, 100),
survive500Waves(SStat.maxWavesSurvived, 500),
researchAll,
useAllMechs(SStat.zoneMechsUsed, 6),
shockWetEnemy,
killEnemyPhaseWall,
researchRouter,
place10kBlocks(SStat.blocksBuilt, 10 * 1000),
destroy1kBlocks(SStat.blocksDestroyed, 1000),
overheatReactor(SStat.reactorsOverheated, 1),
make10maps(SStat.mapsMade, 10),
downloadMapWorkshop,
publishMap(SStat.mapsPublished, 1),
defeatBoss(SStat.bossesDefeated, 1),
unlockAllZones,
configAllZones,
drop10kitems,
powerupImpactReactor,
obtainThorium,
obtainTitanium,
suicideBomb,
buildDaggerFactory,
issueAttackCommand,
active100Units(SStat.maxUnitActive, 100),
active10Phantoms,
active50Crawlers,
build1000Units,
earnSRank,
earnSSRank,
dieExclusion,
drown,
fillCoreAllCampaign,
hostServer10(SStat.maxPlayersServer, 10),
buildMeltdownSpectre,
launchItemPad,
skipLaunching2Death,
chainRouters,
survive10WavesNoBlocks,
useFlameAmmo,
coolTurret,
enablePixelation,
openWiki,
;
private final SStat stat;
private final int statGoal;
public static final SAchievement[] all = values();
/** Creates an achievement that is triggered when this stat reaches a number.*/
SAchievement(SStat stat, int goal){
this.stat = stat;
this.statGoal = goal;
}
SAchievement(){
this(null, 0);
}
public void complete(){
if(!isAchieved()){
SVars.stats.stats.setAchievement(name());
SVars.stats.stats.storeStats();
}
}
public void checkCompletion(){
if(!isAchieved() && stat != null && stat.get() >= statGoal){
complete();
}
}
public boolean isAchieved(){
return SVars.stats.stats.isAchieved(name(), false);
}
}

View File

@@ -0,0 +1,478 @@
package mindustry.desktop.steam;
import arc.*;
import com.codedisaster.steamworks.*;
import com.codedisaster.steamworks.SteamFriends.*;
import com.codedisaster.steamworks.SteamMatchmaking.*;
import com.codedisaster.steamworks.SteamNetworking.*;
import arc.struct.*;
import arc.func.*;
import arc.util.*;
import arc.util.pooling.*;
import mindustry.core.GameState.*;
import mindustry.core.Version;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.net.ArcNetProvider.*;
import mindustry.net.*;
import mindustry.net.Net.*;
import mindustry.net.Packets.*;
import java.io.*;
import java.nio.*;
import java.util.concurrent.*;
import static mindustry.Vars.*;
public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, SteamFriendsCallback, NetProvider{
public final SteamNetworking snet = new SteamNetworking(this);
public final SteamMatchmaking smat = new SteamMatchmaking(this);
public final SteamFriends friends = new SteamFriends(this);
final NetProvider provider;
final PacketSerializer serializer = new PacketSerializer();
final ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024 * 4);
final ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024 * 4);
final CopyOnWriteArrayList<SteamConnection> connections = new CopyOnWriteArrayList<>();
final IntMap<SteamConnection> steamConnections = new IntMap<>(); //maps steam ID -> valid net connection
SteamID currentLobby, currentServer;
Cons<Host> lobbyCallback;
Runnable lobbyDoneCallback, joinCallback;
public SNet(NetProvider provider){
this.provider = provider;
Events.on(ClientLoadEvent.class, e -> Core.app.addListener(new ApplicationListener(){
//read packets
int length;
SteamID from = new SteamID();
@Override
public void update(){
while((length = snet.isP2PPacketAvailable(0)) != 0){
try{
readBuffer.position(0);
snet.readP2PPacket(from, readBuffer, 0);
int fromID = from.getAccountID();
Object output = serializer.read(readBuffer);
if(net.server()){
SteamConnection con = steamConnections.get(fromID);
try{
//accept users on request
if(con == null){
con = new SteamConnection(SteamID.createFromNativeHandle(from.handle()));
Connect c = new Connect();
c.addressTCP = "steam:" + from.getAccountID();
Log.info("&bRecieved STEAM connection: {0}", c.addressTCP);
steamConnections.put(from.getAccountID(), con);
connections.add(con);
net.handleServerReceived(con, c);
}
net.handleServerReceived(con, output);
}catch(RuntimeException e){
if(e.getCause() instanceof ValidateException){
ValidateException v = (ValidateException)e.getCause();
Log.err("Validation failed: {0} ({1})", v.player.name, v.getMessage());
}else{
Log.err(e);
}
}catch(Exception e){
Log.err(e);
}
}else if(currentServer != null && fromID == currentServer.getAccountID()){
net.handleClientReceived(output);
}
}catch(SteamException e){
e.printStackTrace();
}
}
}
}));
Events.on(WaveEvent.class, e -> {
if(currentLobby != null && net.server()){
smat.setLobbyData(currentLobby, "wave", state.wave + "");
}
});
}
public boolean isSteamClient(){
return currentServer != null;
}
@Override
public void connectClient(String ip, int port, Runnable success) throws IOException{
if(ip.startsWith("steam:")){
String lobbyname = ip.substring("steam:".length());
try{
SteamID lobby = SteamID.createFromNativeHandle(Long.parseLong(lobbyname));
joinCallback = success;
smat.joinLobby(lobby);
}catch(NumberFormatException e){
throw new IOException("Invalid Steam ID: " + lobbyname);
}
}else{
provider.connectClient(ip, port, success);
}
}
@Override
public void sendClient(Object object, SendMode mode){
if(isSteamClient()){
if(currentServer == null){
Log.info("Not connected, quitting.");
return;
}
try{
writeBuffer.limit(writeBuffer.capacity());
writeBuffer.position(0);
serializer.write(writeBuffer, object);
writeBuffer.flip();
snet.sendP2PPacket(currentServer, writeBuffer, mode == SendMode.tcp ? P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0);
}catch(Exception e){
net.showError(e);
}
Pools.free(object);
}else{
provider.sendClient(object, mode);
}
}
@Override
public void disconnectClient(){
if(isSteamClient()){
if(currentLobby != null){
smat.leaveLobby(currentLobby);
snet.closeP2PSessionWithUser(currentServer);
currentServer = null;
currentLobby = null;
net.handleClientReceived(new Disconnect());
}
}else{
provider.disconnectClient();
}
}
@Override
public void discoverServers(Cons<Host> callback, Runnable done){
smat.addRequestLobbyListResultCountFilter(32);
smat.requestLobbyList();
lobbyCallback = callback;
lobbyDoneCallback = done;
}
@Override
public void pingHost(String address, int port, Cons<Host> valid, Cons<Exception> failed){
provider.pingHost(address, port, valid, failed);
}
@Override
public void hostServer(int port) throws IOException{
provider.hostServer(port);
smat.createLobby(Core.settings.getBool("publichost") ? LobbyType.Public : LobbyType.FriendsOnly, 16);
Core.app.post(() -> Core.app.post(() -> Core.app.post(() -> Log.info("Server: {0}\nClient: {1}\nActive: {2}", net.server(), net.client(), net.active()))));
}
public void updateLobby(){
if(currentLobby != null && net.server()){
smat.setLobbyType(currentLobby, Core.settings.getBool("publichost") ? LobbyType.Public : LobbyType.FriendsOnly);
}
}
@Override
public void closeServer(){
provider.closeServer();
if(currentLobby != null){
smat.leaveLobby(currentLobby);
for(SteamConnection con : steamConnections.values()){
con.close();
}
currentLobby = null;
}
steamConnections.clear();
}
@Override
public Iterable<? extends NetConnection> getConnections(){
//merge provider connections
CopyOnWriteArrayList<NetConnection> connectionsOut = new CopyOnWriteArrayList<>(connections);
for(NetConnection c : provider.getConnections()) connectionsOut.add(c);
return connectionsOut;
}
void disconnectSteamUser(SteamID steamid){
//a client left
int sid = steamid.getAccountID();
snet.closeP2PSessionWithUser(steamid);
if(steamConnections.containsKey(sid)){
SteamConnection con = steamConnections.get(sid);
net.handleServerReceived(con, new Disconnect());
steamConnections.remove(sid);
connections.remove(con);
}
}
@Override
public void onFavoritesListChanged(int i, int i1, int i2, int i3, int i4, boolean b, int i5){
}
@Override
public void onLobbyInvite(SteamID steamIDUser, SteamID steamIDLobby, long gameID){
Log.info("onLobbyInvite {0} {1} {2}", steamIDLobby.getAccountID(), steamIDUser.getAccountID(), gameID);
}
@Override
public void onLobbyEnter(SteamID steamIDLobby, int chatPermissions, boolean blocked, ChatRoomEnterResponse response){
Log.info("enter lobby {0} {1}", steamIDLobby.getAccountID(), response);
if(response != ChatRoomEnterResponse.Success){
ui.loadfrag.hide();
ui.showErrorMessage(Core.bundle.format("cantconnect", response.toString()));
return;
}
if(net.active()){
net.disconnect();
net.closeServer();
logic.reset();
state.set(State.menu);
}
currentLobby = steamIDLobby;
currentServer = smat.getLobbyOwner(steamIDLobby);
Log.info("Connect to owner {0}: {1}", currentServer.getAccountID(), friends.getFriendPersonaName(currentServer));
if(joinCallback != null){
joinCallback.run();
joinCallback = null;
}
Connect con = new Connect();
con.addressTCP = "steam:" + currentServer.getAccountID();
net.setClientConnected();
net.handleClientReceived(con);
Core.app.post(() -> Core.app.post(() -> Core.app.post(() -> Log.info("Server: {0}\nClient: {1}\nActive: {2}", net.server(), net.client(), net.active()))));
}
@Override
public void onLobbyDataUpdate(SteamID steamID, SteamID steamID1, boolean b){
}
@Override
public void onLobbyChatUpdate(SteamID lobby, SteamID who, SteamID changer, ChatMemberStateChange change){
Log.info("lobby {0}: {1} caused {2}'s change: {3}", lobby.getAccountID(), who.getAccountID(), changer.getAccountID(), change);
if(change == ChatMemberStateChange.Disconnected || change == ChatMemberStateChange.Left){
if(net.client()){
//host left, leave as well
if(who.equals(currentServer) || who.equals(currentLobby)){
net.disconnect();
Log.info("Current host left.");
}
}else{
//a client left
disconnectSteamUser(who);
}
}
}
@Override
public void onLobbyChatMessage(SteamID steamID, SteamID steamID1, ChatEntryType chatEntryType, int i){
}
@Override
public void onLobbyGameCreated(SteamID steamID, SteamID steamID1, int i, short i1){
}
@Override
public void onLobbyMatchList(int matches){
Log.info("found {0} matches {1}", matches, lobbyDoneCallback);
if(lobbyDoneCallback != null){
Array<Host> hosts = new Array<>();
for(int i = 0; i < matches; i++){
try{
SteamID lobby = smat.getLobbyByIndex(i);
Host out = new Host(
smat.getLobbyData(lobby, "name"),
"steam:" + lobby.handle(),
smat.getLobbyData(lobby, "mapname"),
Strings.parseInt(smat.getLobbyData(lobby, "wave"), -1),
smat.getNumLobbyMembers(lobby),
Strings.parseInt(smat.getLobbyData(lobby, "version"), -1),
smat.getLobbyData(lobby, "versionType"),
Gamemode.valueOf(smat.getLobbyData(lobby, "gamemode")),
smat.getLobbyMemberLimit(lobby)
);
hosts.add(out);
}catch(Exception e){
Log.err(e);
}
}
hosts.sort(Structs.comparingInt(h -> -h.players));
hosts.each(lobbyCallback);
lobbyDoneCallback.run();
}
}
@Override
public void onLobbyKicked(SteamID steamID, SteamID steamID1, boolean b){
Log.info("Kicked: {0} {1} {2}", steamID, steamID1, b);
}
@Override
public void onLobbyCreated(SteamResult result, SteamID steamID){
if(!net.server()){
Log.info("Lobby created on server: {0}, ignoring.", steamID);
return;
}
Log.info("Lobby {1} created? {0}", result, steamID.getAccountID());
if(result == SteamResult.OK){
currentLobby = steamID;
smat.setLobbyData(steamID, "name", player.name);
smat.setLobbyData(steamID, "mapname", world.getMap() == null ? "Unknown" : state.rules.zone == null ? world.getMap().name() : state.rules.zone.localizedName);
smat.setLobbyData(steamID, "version", Version.build + "");
smat.setLobbyData(steamID, "versionType", Version.type);
smat.setLobbyData(steamID, "wave", state.wave + "");
smat.setLobbyData(steamID, "gamemode", Gamemode.bestFit(state.rules).name() + "");
}
}
public void showFriendInvites(){
if(currentLobby != null){
friends.activateGameOverlayInviteDialog(currentLobby);
Log.info("Activating overlay dialog");
}
}
@Override
public void onFavoritesListAccountsUpdated(SteamResult steamResult){
}
@Override
public void onP2PSessionConnectFail(SteamID steamIDRemote, P2PSessionError sessionError){
if(net.server()){
Log.info("{0} has disconnected: {1}", steamIDRemote.getAccountID(), sessionError);
disconnectSteamUser(steamIDRemote);
}else if(steamIDRemote.equals(currentServer)){
Log.info("Disconnected! {1}: {0}", steamIDRemote.getAccountID(), sessionError);
net.handleClientReceived(new Disconnect());
}
}
@Override
public void onP2PSessionRequest(SteamID steamIDRemote){
Log.info("Connection request: {0}", steamIDRemote.getAccountID());
if(net.server()){
Log.info("Am server, accepting request from " + steamIDRemote.getAccountID());
snet.acceptP2PSessionWithUser(steamIDRemote);
}
}
@Override
public void onSetPersonaNameResponse(boolean b, boolean b1, SteamResult steamResult){
}
@Override
public void onPersonaStateChange(SteamID steamID, PersonaChange personaChange){
}
@Override
public void onGameOverlayActivated(boolean b){
}
@Override
public void onGameLobbyJoinRequested(SteamID lobby, SteamID steamIDFriend){
Log.info("onGameLobbyJoinRequested {0} {1}", lobby, steamIDFriend);
smat.joinLobby(lobby);
}
@Override
public void onAvatarImageLoaded(SteamID steamID, int i, int i1, int i2){
}
@Override
public void onFriendRichPresenceUpdate(SteamID steamID, int i){
}
@Override
public void onGameRichPresenceJoinRequested(SteamID steamID, String connect){
Log.info("onGameRichPresenceJoinRequested {0} {1}", steamID, connect);
}
@Override
public void onGameServerChangeRequested(String server, String password){
}
public class SteamConnection extends NetConnection{
final SteamID sid;
final P2PSessionState state = new P2PSessionState();
public SteamConnection(SteamID sid){
super(sid.getAccountID() + "");
this.sid = sid;
Log.info("Create STEAM client {0}", sid.getAccountID());
}
@Override
public void send(Object object, SendMode mode){
try{
writeBuffer.limit(writeBuffer.capacity());
writeBuffer.position(0);
serializer.write(writeBuffer, object);
writeBuffer.flip();
snet.sendP2PPacket(sid, writeBuffer, mode == SendMode.tcp ? object instanceof StreamChunk ? P2PSend.ReliableWithBuffering : P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0);
}catch(Exception e){
Log.err(e);
Log.info("Error sending packet. Disconnecting invalid client!");
close();
SteamConnection k = steamConnections.get(sid.getAccountID());
if(k != null) steamConnections.remove(sid.getAccountID());
}
}
@Override
public boolean isConnected(){
snet.getP2PSessionState(sid, state);
return true;//state.isConnectionActive();
}
@Override
public void close(){
disconnectSteamUser(sid);
}
}
}

View File

@@ -0,0 +1,44 @@
package mindustry.desktop.steam;
public enum SStat{
unitsDestroyed,
attacksWon,
pvpsWon,
timesLaunched,
zoneMechsUsed,
blocksDestroyed,
itemsLaunched,
reactorsOverheated,
maxUnitActive,
unitsBuilt,
bossesDefeated,
maxPlayersServer,
mapsMade,
mapsPublished,
maxWavesSurvived,
blocksBuilt,
;
public int get(){
return SVars.stats.stats.getStatI(name(), 0);
}
public void max(int amount){
if(amount > get()){
add(amount - get());
}
}
public void add(int amount){
SVars.stats.stats.setStatI(name(), get() + amount);
SVars.stats.onUpdate();
for(SAchievement a : SAchievement.all){
a.checkCompletion();
}
}
public void add(){
add(1);
}
}

View File

@@ -0,0 +1,329 @@
package mindustry.desktop.steam;
import arc.*;
import com.codedisaster.steamworks.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.entities.type.*;
import mindustry.entities.units.*;
import mindustry.game.EventType.*;
import mindustry.game.Stats.*;
import mindustry.type.*;
import mindustry.world.*;
import static mindustry.Vars.*;
import static mindustry.desktop.steam.SAchievement.*;
@SuppressWarnings("unchecked")
public class SStats implements SteamUserStatsCallback{
public final SteamUserStats stats = new SteamUserStats(this);
private boolean updated = false;
private ObjectSet<String> mechs = new ObjectSet<>();
private int statSavePeriod = 4; //in minutes
public SStats(){
stats.requestCurrentStats();
Events.on(ClientLoadEvent.class, e -> {
mechs = Core.settings.getObject("mechs", ObjectSet.class, ObjectSet::new);
Core.app.addListener(new ApplicationListener(){
Interval i = new Interval();
@Override
public void update(){
if(i.get(60f / 4f)){
checkUpdate();
}
}
});
Timer.schedule(() -> {
if(updated){
stats.storeStats();
}
}, statSavePeriod * 60, statSavePeriod * 60);
});
}
public void onUpdate(){
this.updated = true;
}
private void checkUpdate(){
if(campaign()){
SStat.maxUnitActive.max(unitGroups[player.getTeam().ordinal()].size());
if(unitGroups[player.getTeam().ordinal()].count(u -> u.getType() == UnitTypes.phantom) >= 10){
active10Phantoms.complete();
}
if(unitGroups[player.getTeam().ordinal()].count(u -> u.getType() == UnitTypes.crawler) >= 50){
active50Crawlers.complete();
}
for(Tile tile : state.teams.get(player.getTeam()).cores){
if(!content.items().contains(i -> i.type == ItemType.material && tile.entity.items.get(i) < tile.block().itemCapacity)){
fillCoreAllCampaign.complete();
break;
}
}
}
}
private void registerEvents(){
Events.on(UnitDestroyEvent.class, e -> {
if(ncustom()){
if(e.unit.getTeam() != Vars.player.getTeam()){
SStat.unitsDestroyed.add();
if(e.unit instanceof BaseUnit && ((BaseUnit)e.unit).isBoss()){
SStat.bossesDefeated.add();
}
}
}
});
Events.on(ZoneConfigureCompleteEvent.class, e -> {
if(!content.zones().contains(z -> !z.canConfigure())){
configAllZones.complete();
}
});
Events.on(Trigger.newGame, () -> Core.app.post(() -> {
if(campaign() && player.getClosestCore() != null && player.getClosestCore().items.total() >= 10 * 1000){
drop10kitems.complete();
}
}));
Events.on(CommandIssueEvent.class, e -> {
if(campaign() && e.command == UnitCommand.attack){
issueAttackCommand.complete();
}
});
Events.on(BlockBuildEndEvent.class, e -> {
if(campaign() && e.player == player && !e.breaking){
SStat.blocksBuilt.add();
if(e.tile.block() == Blocks.router && e.tile.entity.proximity().contains(t -> t.block() == Blocks.router)){
chainRouters.complete();
}
if(e.tile.block() == Blocks.daggerFactory){
buildDaggerFactory.complete();
}
if(e.tile.block() == Blocks.meltdown || e.tile.block() == Blocks.spectre){
if(e.tile.block() == Blocks.meltdown && !Core.settings.getBool("meltdownp", false)){
Core.settings.putSave("meltdownp", true);
}
if(e.tile.block() == Blocks.spectre && !Core.settings.getBool("spectrep", false)){
Core.settings.putSave("spectrep", true);
}
if(Core.settings.getBool("meltdownp", false) && Core.settings.getBool("spectrep", false)){
buildMeltdownSpectre.complete();
}
}
}
});
Events.on(BlockDestroyEvent.class, e -> {
if(campaign() && e.tile.getTeam() != player.getTeam()){
SStat.blocksDestroyed.add();
}
});
Events.on(MapMakeEvent.class, e -> SStat.mapsMade.add());
Events.on(MapPublishEvent.class, e -> SStat.mapsPublished.add());
Events.on(UnlockEvent.class, e -> {
if(e.content == Items.thorium) obtainThorium.complete();
if(e.content == Items.titanium) obtainTitanium.complete();
if(!content.zones().contains(Zone::locked)){
unlockAllZones.complete();
}
});
Events.on(Trigger.openWiki, openWiki::complete);
Events.on(Trigger.exclusionDeath, dieExclusion::complete);
Events.on(Trigger.drown, drown::complete);
trigger(Trigger.impactPower, powerupImpactReactor);
trigger(Trigger.flameAmmo, useFlameAmmo);
trigger(Trigger.turretCool, coolTurret);
trigger(Trigger.suicideBomb, suicideBomb);
Events.on(Trigger.enablePixelation, enablePixelation::complete);
Events.on(Trigger.thoriumReactorOverheat, () -> {
if(campaign()){
SStat.reactorsOverheated.add();
}
});
trigger(Trigger.shock, shockWetEnemy);
trigger(Trigger.phaseDeflectHit, killEnemyPhaseWall);
trigger(Trigger.itemLaunch, launchItemPad);
Events.on(UnitCreateEvent.class, e -> {
if(campaign() && e.unit.getTeam() == player.getTeam()){
SStat.unitsBuilt.add();
}
});
Events.on(LoseEvent.class, e -> {
if(campaign()){
if(world.getZone().metCondition() && (state.wave - world.getZone().conditionWave) / world.getZone().launchPeriod >= 1){
skipLaunching2Death.complete();
}
}
});
Events.on(LaunchEvent.class, e -> {
if(state.rules.tutorial){
completeTutorial.complete();
}
SStat.timesLaunched.add();
});
Events.on(LaunchItemEvent.class, e -> {
SStat.itemsLaunched.add(e.stack.amount);
});
Events.on(WaveEvent.class, e -> {
if(ncustom()){
SStat.maxWavesSurvived.max(Vars.state.wave);
if(state.stats.buildingsBuilt == 0 && state.wave >= 10){
survive10WavesNoBlocks.complete();
}
}
});
Events.on(PlayerJoin.class, e -> {
if(Vars.net.server()){
SStat.maxPlayersServer.max(Vars.playerGroup.size());
}
});
Events.on(ResearchEvent.class, e -> {
if(e.content == Blocks.router) researchRouter.complete();
if(!TechTree.all.contains(t -> t.block.locked())){
researchAll.complete();
}
});
Events.on(WinEvent.class, e -> {
if(campaign()){
if(Vars.state.wave <= 5 && state.rules.attackMode){
defeatAttack5Waves.complete();
}
if(Vars.state.rules.attackMode){
SStat.attacksWon.add();
}
RankResult result = state.stats.calculateRank(world.getZone(), state.launched);
if(result.rank == Rank.S) earnSRank.complete();
if(result.rank == Rank.SS) earnSSRank.complete();
}
if(state.rules.pvp){
SStat.pvpsWon.add();
}
});
Events.on(MechChangeEvent.class, e -> {
if(campaign()){
if(mechs.add(e.mech.name)){
SStat.zoneMechsUsed.max(mechs.size);
Core.settings.putObject("mechs", mechs);
Core.settings.save();
}
}
});
}
private void trigger(Trigger trigger, SAchievement ach){
Events.on(trigger, () -> {
if(campaign()){
ach.complete();
}
});
}
private boolean ncustom(){
return campaign();
}
private boolean campaign(){
return Vars.world.isZone();
}
@Override
public void onUserStatsReceived(long gameID, SteamID steamID, SteamResult result){
registerEvents();
if(result != SteamResult.OK){
Log.err("Failed to recieve steam stats: {0}", result);
}else{
Log.err("Recieved steam stats.");
}
}
@Override
public void onUserStatsStored(long gameID, SteamResult result){
Log.info("Stored stats: {0}", result);
if(result == SteamResult.OK){
updated = true;
}
}
@Override
public void onUserStatsUnloaded(SteamID steamID){
}
@Override
public void onUserAchievementStored(long l, boolean b, String s, int i, int i1){
}
@Override
public void onLeaderboardFindResult(SteamLeaderboardHandle steamLeaderboardHandle, boolean b){
}
@Override
public void onLeaderboardScoresDownloaded(SteamLeaderboardHandle steamLeaderboardHandle, SteamLeaderboardEntriesHandle steamLeaderboardEntriesHandle, int i){
}
@Override
public void onLeaderboardScoreUploaded(boolean b, SteamLeaderboardHandle steamLeaderboardHandle, int i, boolean b1, int i1, int i2){
}
@Override
public void onGlobalStatsReceived(long l, SteamResult steamResult){
}
}

View File

@@ -0,0 +1,23 @@
package mindustry.desktop.steam;
import com.codedisaster.steamworks.*;
import com.codedisaster.steamworks.SteamAuth.*;
public class SUser implements SteamUserCallback{
public final SteamUser user = new SteamUser(this);
@Override
public void onValidateAuthTicket(SteamID steamID, AuthSessionResponse authSessionResponse, SteamID ownerSteamID){
}
@Override
public void onMicroTxnAuthorization(int appID, long orderID, boolean authorized){
}
@Override
public void onEncryptedAppTicket(SteamResult result){
}
}

View File

@@ -0,0 +1,10 @@
package mindustry.desktop.steam;
public class SVars{
public final static int steamID = 1127400;
public static SNet net;
public static SStats stats;
public static SWorkshop workshop;
public static SUser user;
}

View File

@@ -0,0 +1,356 @@
package mindustry.desktop.steam;
import arc.*;
import com.codedisaster.steamworks.*;
import com.codedisaster.steamworks.SteamRemoteStorage.*;
import com.codedisaster.steamworks.SteamUGC.*;
import arc.struct.*;
import arc.files.*;
import arc.func.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.maps.*;
import mindustry.mod.Mods.*;
import mindustry.type.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.*;
public class SWorkshop implements SteamUGCCallback{
public final SteamUGC ugc = new SteamUGC(this);
private ObjectMap<Class<? extends Publishable>, Array<Fi>> workshopFiles = new ObjectMap<>();
private ObjectMap<SteamUGCQuery, Cons2<Array<SteamUGCDetails>, SteamResult>> detailHandlers = new ObjectMap<>();
private Array<Cons<SteamPublishedFileID>> itemHandlers = new Array<>();
private ObjectMap<SteamPublishedFileID, Runnable> updatedHandlers = new ObjectMap<>();
public SWorkshop(){
int items = ugc.getNumSubscribedItems();
SteamPublishedFileID[] ids = new SteamPublishedFileID[items];
ItemInstallInfo info = new ItemInstallInfo();
ugc.getSubscribedItems(ids);
Array<Fi> folders = Array.with(ids).map(f -> {
ugc.getItemInstallInfo(f, info);
return new Fi(info.getFolder());
}).select(f -> f != null && f.list().length > 0);
workshopFiles.put(Map.class, folders.select(f -> f.list().length == 1 && f.list()[0].extension().equals(mapExtension)).map(f -> f.list()[0]));
workshopFiles.put(Schematic.class, folders.select(f -> f.list().length == 1 && f.list()[0].extension().equals(schematicExtension)).map(f -> f.list()[0]));
workshopFiles.put(LoadedMod.class, folders.select(f -> f.child("mod.json").exists() || f.child("mod.hjson").exists()));
if(!workshopFiles.get(Map.class).isEmpty()){
SAchievement.downloadMapWorkshop.complete();
}
workshopFiles.each((type, list) -> {
Log.info("Fetched content ({0}): {1}", type.getSimpleName(), list.size);
});
}
public Array<Fi> getWorkshopFiles(Class<? extends Publishable> type){
return workshopFiles.getOr(type, () -> new Array<>(0));
}
/** Publish a new item and submit an update for it.
* If it is already published, redirects to its page.*/
public void publish(Publishable p){
Log.info("publish(): " + p.steamTitle());
if(p.hasSteamID()){
Log.info("Content already published, redirecting to ID.");
viewListing(p);
return;
}
if(!p.prePublish()){
Log.info("Rejecting due to pre-publish.");
return;
}
showPublish(id -> update(p, id, null));
}
/** Update an existing item with a changelog. */
public void updateItem(Publishable p, String changelog){
String id = p.getSteamID();
long handle = Strings.parseLong(id, -1);
update(p, new SteamPublishedFileID(handle), changelog);
}
/** Fetches info for an item, checking to make sure that it exists.*/
public void viewListing(Publishable p){
long handle = Strings.parseLong(p.getSteamID(), -1);
SteamPublishedFileID id = new SteamPublishedFileID(handle);
Log.info("Handle = " + handle);
ui.loadfrag.show();
query(ugc.createQueryUGCDetailsRequest(id), (detailsList, result) -> {
ui.loadfrag.hide();
Log.info("Fetch result = " + result);
if(result == SteamResult.OK){
SteamUGCDetails details = detailsList.first();
Log.info("Details result = " + details.getResult());
if(details.getResult() == SteamResult.OK){
if(details.getOwnerID().equals(SVars.user.user.getSteamID())){
FloatingDialog dialog = new FloatingDialog("$workshop.info");
dialog.setFillParent(false);
dialog.cont.add("$workshop.menu").pad(20f);
dialog.addCloseButton();
dialog.buttons.addImageTextButton("$view.workshop", Icon.linkSmall, () -> {
viewListingID(id);
dialog.hide();
}).size(210f, 64f);
dialog.buttons.addImageTextButton("$workshop.update", Icon.upgradeSmall, () -> {
new FloatingDialog("$workshop.update"){{
setFillParent(false);
cont.margin(10).add("$changelog").padRight(6f);
cont.row();
TextArea field = cont.addArea("", t -> {}).size(500f, 160f).get();
field.setMaxLength(400);
buttons.defaults().size(120, 54).pad(4);
buttons.addButton("$ok", () -> {
if(!p.prePublish()){
Log.info("Rejecting due to pre-publish.");
return;
}
ui.loadfrag.show("$publishing");
updateItem(p, field.getText().replace("\r", "\n"));
dialog.hide();
hide();
});
buttons.addButton("$cancel", this::hide);
}}.show();
}).size(210f, 64f);
dialog.show();
}else{
SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + details.getPublishedFileID().handle());
}
}else if(details.getResult() == SteamResult.FileNotFound){
p.removeSteamID();
ui.showErrorMessage("$missing");
}else{
ui.showErrorMessage(Core.bundle.format("workshop.error", details.getResult().name()));
}
}else{
ui.showErrorMessage(Core.bundle.format("workshop.error", result.name()));
}
});
}
void viewListingID(SteamPublishedFileID id){
SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + id.handle());
}
void update(Publishable p, SteamPublishedFileID id, String changelog){
Log.info("Calling update({0}) {1}", p.steamTitle(), id.handle());
String sid = id.handle() + "";
updateItem(id, h -> {
if(p.steamDescription() != null){
ugc.setItemDescription(h, p.steamDescription());
}
Array<String> tags = p.extraTags();
tags.add(p.steamTag());
ugc.setItemTitle(h, p.steamTitle());
ugc.setItemTags(h, tags.toArray(String.class));
ugc.setItemPreview(h, p.createSteamPreview(sid).absolutePath());
ugc.setItemContent(h, p.createSteamFolder(sid).absolutePath());
if(changelog == null){
ugc.setItemVisibility(h, PublishedFileVisibility.Private);
}
ugc.submitItemUpdate(h, changelog == null ? "<Created>" : changelog);
if(p instanceof Map){
SAchievement.publishMap.complete();
}
}, () -> p.addSteamID(sid));
}
void showPublish(Cons<SteamPublishedFileID> published){
FloatingDialog dialog = new FloatingDialog("$confirm");
dialog.setFillParent(false);
dialog.cont.add("$publish.confirm").width(600f).wrap();
dialog.addCloseButton();
dialog.buttons.addImageTextButton("$eula", Icon.linkSmall,
() -> SVars.net.friends.activateGameOverlayToWebPage("https://steamcommunity.com/sharedfiles/workshoplegalagreement"))
.size(210f, 64f);
dialog.buttons.addImageTextButton("$ok", Icon.checkSmall, () -> {
Log.info("Accepted, publishing item...");
itemHandlers.add(published);
ugc.createItem(SVars.steamID, WorkshopFileType.Community);
ui.loadfrag.show("$publishing");
dialog.hide();
}).size(170f, 64f);
dialog.show();
}
void query(SteamUGCQuery query, Cons2<Array<SteamUGCDetails>, SteamResult> handler){
Log.info("POST QUERY " + query);
detailHandlers.put(query, handler);
ugc.sendQueryUGCRequest(query);
}
void updateItem(SteamPublishedFileID publishedFileID, Cons<SteamUGCUpdateHandle> tagger, Runnable updated){
try{
SteamUGCUpdateHandle h = ugc.startItemUpdate(SVars.steamID, publishedFileID);
Log.info("begin updateItem({0})", publishedFileID.handle());
tagger.get(h);
Log.info("Tagged.");
ItemUpdateInfo info = new ItemUpdateInfo();
ui.loadfrag.setProgress(() -> {
ItemUpdateStatus status = ugc.getItemUpdateProgress(h, info);
ui.loadfrag.setText("$" + status.name().toLowerCase());
if(status == ItemUpdateStatus.Invalid){
ui.loadfrag.setText("$done");
return 1f;
}
return (float)status.ordinal() / (float)ItemUpdateStatus.values().length;
});
updatedHandlers.put(publishedFileID, updated);
}catch(Throwable t){
ui.loadfrag.hide();
Log.err(t);
}
}
@Override
public void onRequestUGCDetails(SteamUGCDetails details, SteamResult result){
}
@Override
public void onUGCQueryCompleted(SteamUGCQuery query, int numResultsReturned, int totalMatchingResults, boolean isCachedData, SteamResult result){
Log.info("GET QUERY " + query);
if(detailHandlers.containsKey(query)){
Log.info("Query being handled...");
if(numResultsReturned > 0){
Log.info("{0} q results", numResultsReturned);
Array<SteamUGCDetails> details = new Array<>();
for(int i = 0; i < numResultsReturned; i++){
details.add(new SteamUGCDetails());
ugc.getQueryUGCResult(query, i, details.get(i));
}
detailHandlers.get(query).get(details, result);
}else{
Log.info("Nothing found.");
detailHandlers.get(query).get(new Array<>(), SteamResult.FileNotFound);
}
detailHandlers.remove(query);
}else{
Log.info("Query not handled.");
}
}
@Override
public void onSubscribeItem(SteamPublishedFileID publishedFileID, SteamResult result){
ItemInstallInfo info = new ItemInstallInfo();
ugc.getItemInstallInfo(publishedFileID, info);
Log.info("Item subscribed from {0}", info.getFolder());
SAchievement.downloadMapWorkshop.complete();
}
@Override
public void onUnsubscribeItem(SteamPublishedFileID publishedFileID, SteamResult result){
ItemInstallInfo info = new ItemInstallInfo();
ugc.getItemInstallInfo(publishedFileID, info);
Log.info("Item unsubscribed from {0}", info.getFolder());
}
@Override
public void onCreateItem(SteamPublishedFileID publishedFileID, boolean needsToAcceptWLA, SteamResult result){
Log.info("onCreateItem(" + result + ")");
if(!itemHandlers.isEmpty()){
if(result == SteamResult.OK){
Log.info("Passing to first handler.");
itemHandlers.first().get(publishedFileID);
}else{
ui.showErrorMessage(Core.bundle.format("publish.error", result.name()));
}
itemHandlers.remove(0);
}else{
Log.err("No handlers for createItem()");
}
}
@Override
public void onSubmitItemUpdate(SteamPublishedFileID publishedFileID, boolean needsToAcceptWLA, SteamResult result){
ui.loadfrag.hide();
Log.info("onsubmititemupdate {0} {1} {2}", publishedFileID.handle(), needsToAcceptWLA, result);
if(result == SteamResult.OK){
//redirect user to page for further updates
SVars.net.friends.activateGameOverlayToWebPage("steam://url/CommunityFilePage/" + publishedFileID.handle());
if(needsToAcceptWLA){
SVars.net.friends.activateGameOverlayToWebPage("https://steamcommunity.com/sharedfiles/workshoplegalagreement");
}
if(updatedHandlers.containsKey(publishedFileID)){
updatedHandlers.get(publishedFileID).run();
}
}else{
ui.showErrorMessage(Core.bundle.format("publish.error", result.name()));
}
}
@Override
public void onDownloadItemResult(int appID, SteamPublishedFileID publishedFileID, SteamResult result){
SAchievement.downloadMapWorkshop.complete();
ItemInstallInfo info = new ItemInstallInfo();
ugc.getItemInstallInfo(publishedFileID, info);
Log.info("Item downloaded to {0}", info.getFolder());
}
@Override
public void onUserFavoriteItemsListChanged(SteamPublishedFileID publishedFileID, boolean wasAddRequest, SteamResult result){
}
@Override
public void onSetUserItemVote(SteamPublishedFileID publishedFileID, boolean voteUp, SteamResult result){
}
@Override
public void onGetUserItemVote(SteamPublishedFileID publishedFileID, boolean votedUp, boolean votedDown, boolean voteSkipped, SteamResult result){
}
@Override
public void onStartPlaytimeTracking(SteamResult result){
}
@Override
public void onStopPlaytimeTracking(SteamResult result){
}
@Override
public void onStopPlaytimeTrackingForAllItems(SteamResult result){
}
@Override
public void onDeleteItem(SteamPublishedFileID publishedFileID, SteamResult result){
ItemInstallInfo info = new ItemInstallInfo();
ugc.getItemInstallInfo(publishedFileID, info);
Log.info("Item removed from {0}", info.getFolder());
}
}