diff --git a/lib/framework/wzconfig.cpp b/lib/framework/wzconfig.cpp index 0d599829da6..61935163dfd 100644 --- a/lib/framework/wzconfig.cpp +++ b/lib/framework/wzconfig.cpp @@ -346,3 +346,8 @@ void WzConfig::setValue(const QString &key, const QVariant &value) { mObj.insert(key, QJsonValue::fromVariant(value)); } + +void WzConfig::set(const QString &key, const QJsonValue &value) +{ + mObj.insert(key, value); +} diff --git a/lib/framework/wzconfig.h b/lib/framework/wzconfig.h index 338f7f9160a..3a7c6a403fa 100644 --- a/lib/framework/wzconfig.h +++ b/lib/framework/wzconfig.h @@ -84,6 +84,7 @@ class WzConfig } void setValue(const QString &key, const QVariant &value); + void set(const QString &key, const QJsonValue &value); QString group() { diff --git a/src/cheat.cpp b/src/cheat.cpp index dd3462b6f9f..2253c50d5ef 100644 --- a/src/cheat.cpp +++ b/src/cheat.cpp @@ -46,6 +46,7 @@ static CHEAT_ENTRY cheatCodes[] = {"templates", listTemplates}, // print templates {"jsload", jsAutogame}, // load an AI script for selectedPlayer {"jsdebug", jsShowDebug}, // show scripting states + {"teach us", kf_TeachSelected}, // give experience to selected units {"clone wars", []{ kf_CloneSelected(10); }}, // clone selected units {"clone wars!", []{ kf_CloneSelected(40); }}, // clone selected units {"clone wars!!", []{ kf_CloneSelected(135); }}, // clone selected units diff --git a/src/droid.cpp b/src/droid.cpp index 9467b378082..7b5589c6d63 100644 --- a/src/droid.cpp +++ b/src/droid.cpp @@ -81,7 +81,7 @@ #define DROID_REPAIR_SPREAD (20 - rand()%40) // store the experience of recently recycled droids -UWORD aDroidExperience[MAX_PLAYERS][MAX_RECYCLED_DROIDS]; +static std::priority_queue recycled_experience[MAX_PLAYERS]; /** Height the transporter hovers at above the terrain. */ #define TRANSPORTER_HOVER_HEIGHT 10 @@ -152,7 +152,10 @@ static void droidBodyUpgrade(DROID *psDroid) // initialise droid module bool droidInit() { - memset(aDroidExperience, 0, sizeof(UWORD) * MAX_PLAYERS * MAX_RECYCLED_DROIDS); + for (int i = 0; i < MAX_PLAYERS; i++) + { + recycled_experience[i] = std::priority_queue (); // clear it + } psLastDroidHit = nullptr; return true; @@ -386,32 +389,29 @@ DROID::~DROID() free(sMove.asPath); } +std::priority_queue copy_experience_queue(int player) +{ + return recycled_experience[player]; +} + +void add_to_experience_queue(int player, int value) +{ + recycled_experience[player].push(value); +} // recycle a droid (retain it's experience and some of it's cost) void recycleDroid(DROID *psDroid) { - UDWORD numKills, minKills; - SDWORD i, cost, storeIndex; - Vector3i position; - CHECK_DROID(psDroid); // store the droids kills - numKills = psDroid->experience / 65536; - minKills = UWORD_MAX; - storeIndex = 0; - for (i = 0; i < MAX_RECYCLED_DROIDS; i++) + if (psDroid->experience > 0) { - if (aDroidExperience[psDroid->player][i] < (UWORD)minKills) - { - storeIndex = i; - minKills = aDroidExperience[psDroid->player][i]; - } + recycled_experience[psDroid->player].push(psDroid->experience); } - aDroidExperience[psDroid->player][storeIndex] = (UWORD)numKills; // return part of the cost of the droid - cost = calcDroidPower(psDroid); + int cost = calcDroidPower(psDroid); cost = (cost / 2) * psDroid->body / psDroid->originalBody; addPower(psDroid->player, (UDWORD)cost); @@ -423,13 +423,10 @@ void recycleDroid(DROID *psDroid) psDroid->psGroup->remove(psDroid); } - position.x = psDroid->pos.x; // Add an effect - position.z = psDroid->pos.y; - position.y = psDroid->pos.z; - triggerEvent(TRIGGER_OBJECT_RECYCLED, psDroid); vanishDroid(psDroid); + Vector3i position = psDroid->pos.xzy; addEffect(&position, EFFECT_EXPLOSION, EXPLOSION_TYPE_DISCOVERY, false, nullptr, false, gameTime - deltaGameTime + 1); CHECK_DROID(psDroid); @@ -1624,7 +1621,6 @@ DROID *reallyBuildDroid(DROID_TEMPLATE *pTemplate, Position pos, UDWORD player, { DROID *psDroid; DROID_GROUP *psGrp; - SDWORD i, experienceLoc; // Don't use this assertion in single player, since droids can finish building while on an away mission ASSERT(!bMultiPlayer || worldOnMap(pos.x, pos.y), "the build locations are not on the map"); @@ -1660,20 +1656,11 @@ DROID *reallyBuildDroid(DROID_TEMPLATE *pTemplate, Position pos, UDWORD player, (psDroid->droidType != DROID_CYBORG_CONSTRUCT) && (psDroid->droidType != DROID_REPAIR) && (psDroid->droidType != DROID_CYBORG_REPAIR) && - !isTransporter(psDroid)) + !isTransporter(psDroid) && + !recycled_experience[psDroid->player].empty()) { - uint32_t numKills = 0; - experienceLoc = 0; - for (i = 0; i < MAX_RECYCLED_DROIDS; i++) - { - if (aDroidExperience[player][i] * 65536 > numKills) - { - numKills = aDroidExperience[player][i] * 65536; - experienceLoc = i; - } - } - aDroidExperience[player][experienceLoc] = 0; - psDroid->experience = numKills; + psDroid->experience = recycled_experience[psDroid->player].top(); + recycled_experience[psDroid->player].pop(); } else { diff --git a/src/droid.h b/src/droid.h index b3eb1c66b62..0bd6e0cb995 100644 --- a/src/droid.h +++ b/src/droid.h @@ -31,6 +31,8 @@ #include "stats.h" #include "visibility.h" +#include + #define OFF_SCREEN 9999 // world->screen check - alex #define REPAIRLEV_LOW 50 // percentage of body points remaining at which to repair droid automatically. @@ -58,8 +60,8 @@ enum PICKTILE // the structure that was last hit extern DROID *psLastDroidHit; -extern UWORD aDroidExperience[MAX_PLAYERS][MAX_RECYCLED_DROIDS]; - +std::priority_queue copy_experience_queue(int player); +void add_to_experience_queue(int player, int value); // initialise droid module bool droidInit(); diff --git a/src/game.cpp b/src/game.cpp index f63020aabbb..2d87b9965e1 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -789,7 +789,7 @@ static bool serializeSaveGameV15Data(PHYSFS_file *fileHandle, const SAVE_GAME_V1 { for (j = 0; j < MAX_RECYCLED_DROIDS; ++j) { - if (!PHYSFS_writeUBE8(fileHandle, serializeGame->aDroidExperience[i][j])) + if (!PHYSFS_writeUBE8(fileHandle, 0)) // no longer saved in binary form { return false; } @@ -819,10 +819,15 @@ static bool deserializeSaveGameV15Data(PHYSFS_file *fileHandle, SAVE_GAME_V15 *s { for (j = 0; j < MAX_RECYCLED_DROIDS; ++j) { - if (!PHYSFS_readUBE8(fileHandle, &serializeGame->aDroidExperience[i][j])) + uint8_t tmp; + if (!PHYSFS_readUBE8(fileHandle, &tmp)) { return false; } + if (tmp > 0) + { + add_to_experience_queue(i, tmp * 65536); + } } } @@ -1126,7 +1131,7 @@ static bool serializeSaveGameV27Data(PHYSFS_file *fileHandle, const SAVE_GAME_V2 { for (j = 0; j < MAX_RECYCLED_DROIDS; ++j) { - if (!PHYSFS_writeUBE16(fileHandle, serializeGame->awDroidExperience[i][j])) + if (!PHYSFS_writeUBE16(fileHandle, 0)) { return false; } @@ -1149,10 +1154,15 @@ static bool deserializeSaveGameV27Data(PHYSFS_file *fileHandle, SAVE_GAME_V27 *s { for (j = 0; j < MAX_RECYCLED_DROIDS; ++j) { - if (!PHYSFS_readUBE16(fileHandle, &serializeGame->awDroidExperience[i][j])) + uint16_t tmp; + if (!PHYSFS_readUBE16(fileHandle, &tmp)) { return false; } + if (tmp > 0) + { + add_to_experience_queue(i, tmp * 65536); + } } } @@ -1564,6 +1574,8 @@ static bool IsScenario; /***************************************************************************/ static bool gameLoadV7(PHYSFS_file *fileHandle); static bool gameLoadV(PHYSFS_file *fileHandle, unsigned int version); +static bool loadMainFile(const std::string &fileName); +static bool writeMainFile(const std::string &fileName, SDWORD saveType); static bool writeGameFile(const char *fileName, SDWORD saveType); static bool writeMapFile(const char *fileName); @@ -1612,8 +1624,6 @@ static bool gameLoad(const char *fileName); /* set the global scroll values to use for the save game */ static void setMapScroll(); -static bool gameLoad(const char *fileName); - static char *getSaveStructNameV19(SAVE_STRUCTURE_V17 *psSaveStructure) { return (psSaveStructure->name); @@ -1947,26 +1957,6 @@ bool loadGame(const char *pGameToLoad, bool keepObjects, bool freeMem, bool User } } - if (saveGameVersion >= VERSION_27)//V27 - { - for (player = 0; player < MAX_PLAYERS; player++) - { - for (inc = 0; inc < MAX_RECYCLED_DROIDS; inc++) - { - aDroidExperience[player][inc] = saveGameData.awDroidExperience[player][inc]; - } - } - } - else - { - for (player = 0; player < MAX_PLAYERS; player++) - { - for (inc = 0; inc < MAX_RECYCLED_DROIDS; inc++) - { - aDroidExperience[player][inc] = saveGameData.aDroidExperience[player][inc]; - } - } - } if (saveGameVersion >= VERSION_30) { scrGameLevel = saveGameData.scrGameLevel; @@ -2640,7 +2630,7 @@ bool saveGame(const char *aFileName, GAME_TYPE saveType) /* Write the data to the file */ if (!writeGameFile(CurrentFileName, saveType)) { - debug(LOG_ERROR, "saveGame: writeGameFile(\"%s\") failed", CurrentFileName); + debug(LOG_ERROR, "writeGameFile(\"%s\") failed", CurrentFileName); goto error; } @@ -2650,6 +2640,8 @@ bool saveGame(const char *aFileName, GAME_TYPE saveType) //create dir will fail if directory already exists but don't care! (void) PHYSFS_mkdir(CurrentFileName); + writeMainFile(std::string(CurrentFileName) + "/main.json", saveType); + //save the map file strcat(CurrentFileName, "/game.map"); /* Write the data to the file */ @@ -2945,6 +2937,8 @@ static bool writeMapFile(const char *fileName) // ----------------------------------------------------------------------------------------- static bool gameLoad(const char *fileName) { + char CurrentFileName[PATH_MAX]; + strcpy(CurrentFileName, fileName); GAME_SAVEHEADER fileHeader; PHYSFS_file *fileHandle = openLoadFile(fileName, true); @@ -3034,6 +3028,9 @@ static bool gameLoad(const char *fileName) { bool retVal = gameLoadV(fileHandle, fileHeader.version); PHYSFS_close(fileHandle); + //remove the file extension + CurrentFileName[strlen(CurrentFileName) - 4] = '\0'; + loadMainFile(std::string(CurrentFileName) + "/main.json"); return retVal; } else @@ -3048,7 +3045,7 @@ static bool gameLoad(const char *fileName) // Fix endianness of a savegame static void endian_SaveGameV(SAVE_GAME *psSaveGame, UDWORD version) { - unsigned int i, j; + unsigned int i; /* SAVE_GAME is GAME_SAVE_V33 */ /* GAME_SAVE_V33 includes GAME_SAVE_V31 */ if (version >= VERSION_33) @@ -3079,15 +3076,6 @@ static void endian_SaveGameV(SAVE_GAME *psSaveGame, UDWORD version) endian_uword(&psSaveGame->missionScrollMaxX); endian_uword(&psSaveGame->missionScrollMaxY); } - /* GAME_SAVE_V27 includes GAME_SAVE_V24 */ - if (version >= VERSION_27) - { - for (i = 0; i < MAX_PLAYERS; i++) - for (j = 0; j < MAX_RECYCLED_DROIDS; j++) - { - endian_uword(&psSaveGame->awDroidExperience[i][j]); - } - } /* GAME_SAVE_V24 includes GAME_SAVE_V22 */ if (version >= VERSION_24) { @@ -3739,13 +3727,7 @@ bool gameLoadV(PHYSFS_file *fileHandle, unsigned int version) mission.cheatTime = saveGameData.missionCheatTime; } - for (player = 0; player < MAX_PLAYERS; player++) - { - for (i = 0; i < MAX_RECYCLED_DROIDS; ++i) - { - aDroidExperience[player][i] = 0;//clear experience before - } - } + droidInit(); //set IsScenario to true if not a user saved game if ((gameType == GTYPE_SAVE_START) || @@ -3759,10 +3741,6 @@ bool gameLoadV(PHYSFS_file *fileHandle, unsigned int version) for (player = 0; player < MAX_PLAYERS; player++) { - for (i = 0; i < MAX_RECYCLED_DROIDS; ++i) - { - aDroidExperience[player][i] = 0;//clear experience before building saved units - } NetPlay.players[player].ai = saveGameData.sNetPlay.players[player].ai; NetPlay.players[player].difficulty = saveGameData.sNetPlay.players[player].difficulty; sstrcpy(NetPlay.players[player].name, saveGameData.sNetPlay.players[player].name); @@ -3840,11 +3818,164 @@ bool gameLoadV(PHYSFS_file *fileHandle, unsigned int version) return true; } +// ----------------------------------------------------------------------------------------- +// Load main game data from JSON. Only implement stuff here that we actually use instead of +// the binary blobbery. +static bool loadMainFile(const std::string &fileName) +{ + WzConfig save(QString::fromStdString(fileName), WzConfig::ReadOnly); + + save.beginArray("players"); + while (save.remainingArrayItems() > 0) + { + int index = save.value("index").toInt(); + QVariant value = save.value("recycled_droids"); + for (const QVariant &v : value.toList()) + { + add_to_experience_queue(index, v.toInt()); + } + save.nextArrayItem(); + } + save.endArray(); + return true; +} // ----------------------------------------------------------------------------------------- -/* -Writes the game specifics to a file -*/ +// Save main game data to JSON. We save more here than we need to, and duplicate some of the +// binary blobbery, for future usage. +static bool writeMainFile(const std::string &fileName, SDWORD saveType) +{ + ASSERT(saveType == GTYPE_SAVE_START || saveType == GTYPE_SAVE_MIDMISSION, "invalid save type"); + + WzConfig save(QString::fromStdString(fileName), WzConfig::ReadAndWrite); + + uint32_t saveKey = getCampaignNumber(); + if (missionIsOffworld()) + { + saveKey |= SAVEKEY_ONMISSION; + saveGameOnMission = true; + } + else + { + saveGameOnMission = false; + } + + /* Put the save game data into the buffer */ + save.setValue("version", 1); // version of this file + save.setValue("saveKey", saveKey); + save.setValue("gameTime", gameTime); + save.setValue("missionTime", mission.startTime); + save.setVector2i("scrollMin", Vector2i(scrollMinX, scrollMinY)); + save.setVector2i("scrollMax", Vector2i(scrollMaxX, scrollMaxY)); + save.setValue("saveType", saveType); + save.setValue("levelName", aLevelName); + save.setValue("radarPermitted", radarPermitted); + save.setValue("allowDesign", allowDesign); + save.setValue("missionOffTime", mission.time); + save.setValue("missionETA", mission.ETA); + save.setValue("missionCheatTime", mission.cheatTime); + save.setVector2i("missionHomeLZ", Vector2i(mission.homeLZ_X, mission.homeLZ_Y)); + save.setVector2i("missionPlayerPos", Vector2i(mission.playerX, mission.playerY)); + save.setVector2i("missionScrollMin", Vector2i(mission.scrollMinX, mission.scrollMinY)); + save.setVector2i("missionScrollMax", Vector2i(mission.scrollMaxX, mission.scrollMaxY)); + save.setValue("offWorldKeepLists", offWorldKeepLists); + save.setValue("rubbleTile", getRubbleTileNum()); + save.setValue("waterTile", getWaterTileNum()); + save.setValue("objId", MAX(unsynchObjID * 2, (synchObjID + 3) / 4)); + save.setValue("radarZoom", GetRadarZoom()); + save.setValue("droidsToSafetyFlag", getDroidsToSafetyFlag()); + save.setValue("reinforceTime", missionGetReinforcementTime()); + save.setValue("playCountDown", getPlayCountDown()); + + save.beginArray("players"); + for (int i = 0; i < MAX_PLAYERS; ++i) + { + save.setValue("index", i); + save.setValue("power", getPower(i)); + save.setVector2i("iTranspEntryTile", Vector2i(mission.iTranspEntryTileX[i], mission.iTranspEntryTileY[i])); + save.setVector2i("iTranspExitTile", Vector2i(mission.iTranspExitTileX[i], mission.iTranspExitTileY[i])); + save.setValue("aDefaultSensor", aDefaultSensor[i]); + save.setValue("aDefaultECM", aDefaultECM[i]); + save.setValue("aDefaultRepair", aDefaultRepair[i]); + save.setValue("colour", getPlayerColour(i)); + save.setVector2i("VTOL_return_position", asVTOLReturnPos[i]); + + std::priority_queue experience = copy_experience_queue(i); + QJsonArray recycled_droids; + while (!experience.empty()) + { + recycled_droids.append(experience.top()); + experience.pop(); + } + save.set("recycled_droids", recycled_droids); + + QJsonArray allies; + for (int j = 0; j < MAX_PLAYERS; j++) + { + allies.append(alliances[i][j]); + } + save.set("alliances", allies); + save.setValue("difficulty", game.skDiff[i]); + + // RUN_DATA not saved -- is it still used? + + save.setValue("position", NetPlay.players[i].position); + save.setValue("colour", NetPlay.players[i].colour); + save.setValue("allocated", NetPlay.players[i].allocated); + save.setValue("team", NetPlay.players[i].team); + save.setValue("ai", NetPlay.players[i].ai); + save.setValue("difficultyLevel", NetPlay.players[i].difficulty); + save.setValue("autoGame", NetPlay.players[i].autoGame); + save.setValue("ip", NetPlay.players[i].IPtextAddress); + + // wtf why do we keep player names stored in so many places? + save.setValue("netName", NetPlay.players[i].name); + save.setValue("name", getPlayerName(selectedPlayer)); + + save.nextArrayItem(); + } + save.endArray(); + + iView playerPos; + disp3d_getView(&playerPos); + save.setVector3i("camera_position", playerPos.p); + save.setVector3i("camera_rotation", playerPos.r); + + save.beginArray("landing_zones"); + for (int i = 0; i < MAX_NOGO_AREAS; ++i) + { + LANDING_ZONE *psLandingZone = getLandingZone(i); + save.setVector2i("start", Vector2i(psLandingZone->x1, psLandingZone->x2)); + save.setVector2i("end", Vector2i(psLandingZone->y1, psLandingZone->y2)); + save.nextArrayItem(); + } + save.endArray(); + + save.setValue("playerHasWon", testPlayerHasWon()); + save.setValue("playerHasLost", testPlayerHasLost()); + save.setValue("gameLevel", scrGameLevel); + save.setValue("failFlag", bExtraFailFlag); + save.setValue("trackTransporter", bTrackTransporter); + save.setValue("gameType", game.type); + save.setValue("scavengers", game.scavengers); + save.setValue("mapName", game.map); + save.setValue("maxPlayers", game.maxPlayers); + save.setValue("gameName", game.name); + save.setValue("powerSetting", game.power); + save.setValue("baseSetting", game.base); + save.setValue("allianceSetting", game.alliance); + save.setValue("mapHasScavengers", game.mapHasScavengers); + save.setValue("mapMod", game.isMapMod); + save.setValue("selectedPlayer", selectedPlayer); + save.setValue("multiplayer", bMultiPlayer); + save.setValue("playerCount", NetPlay.playercount); + save.setValue("hostPlayer", NetPlay.hostPlayer); + save.setValue("bComms", NetPlay.bComms); + save.setValue("modList", getModList().c_str()); + + return true; +} + static bool writeGameFile(const char *fileName, SDWORD saveType) { GAME_SAVEHEADER fileHeader; @@ -3876,7 +4007,6 @@ static bool writeGameFile(const char *fileName, SDWORD saveType) } ASSERT(saveType == GTYPE_SAVE_START || saveType == GTYPE_SAVE_MIDMISSION, "invalid save type"); - // saveKeymissionIsOffworld saveGame.saveKey = getCampaignNumber(); if (missionIsOffworld()) { @@ -3943,11 +4073,6 @@ static bool writeGameFile(const char *fileName, SDWORD saveType) saveGame.aDefaultSensor[i] = aDefaultSensor[i]; saveGame.aDefaultECM[i] = aDefaultECM[i]; saveGame.aDefaultRepair[i] = aDefaultRepair[i]; - - for (j = 0; j < MAX_RECYCLED_DROIDS; ++j) - { - saveGame.awDroidExperience[i][j] = aDroidExperience[i][j]; - } } for (i = 0; i < MAX_NOGO_AREAS; ++i) diff --git a/src/keybind.cpp b/src/keybind.cpp index 98e21bc1f94..e66771b3883 100644 --- a/src/keybind.cpp +++ b/src/keybind.cpp @@ -425,6 +425,27 @@ void kf_CloneSelected(int limit) debug(LOG_INFO, "Nothing was selected?"); } } + +void kf_TeachSelected() +{ +#ifndef DEBUG + // Bail out if we're running a _true_ multiplayer game (to prevent MP cheating) + if (runningMultiplayer()) + { + noMPCheatMsg(); + return; + } +#endif + + for (DROID *psDroid = apsDroidLists[selectedPlayer]; psDroid; psDroid = psDroid->psNext) + { + if (psDroid->selected) + { + psDroid->experience += 4 * 65536; + } + } +} + // -------------------------------------------------------------------------- // ///* Prints out the date and time of the build of the game */ diff --git a/src/keybind.h b/src/keybind.h index 5a14ba18c4c..cd0434346fd 100644 --- a/src/keybind.h +++ b/src/keybind.h @@ -234,6 +234,7 @@ void kf_SpeedUp(); void kf_SlowDown(); void kf_NormalSpeed(); +void kf_TeachSelected(); void kf_CloneSelected(int); void kf_Reload();