Last active
January 18, 2024 01:35
-
-
Save json-m/66bccaafd64e5ac75966d0698a336277 to your computer and use it in GitHub Desktop.
project zomboid dedicated server map load order
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
// for Project Zomboid Dedicated Server | |
// converts a ModManager load order string to a map mod name load order | |
// your own client doesn't need the map names since it loads them via the mod load order anyway | |
// however, hosting a server requires this specific Map load directive in servertest.ini | |
// this will output a Map= load order in the same order as the mod load order | |
// the idea is that getting a stable mod load order is way easier in the game client mod manager | |
// then you can export and use this on a dedicated server instance to host a mp game with your mod order | |
// tldr; | |
// 1. replace modOrderString with yours from saved_modlists.txt for your mod saved preset | |
// 2. replace workShopPath with yours (change to forward slashes) | |
// 3. go run mapper.go | |
// 4. copy maps.txt string to Map= setting in servertest.ini (or whatever yours is named) | |
// didn't bother cleaning this up at all, don't really care | |
// enjoy and good luck | |
import ( | |
"bufio" | |
"bytes" | |
"fmt" | |
"log" | |
"os" | |
"path/filepath" | |
"strings" | |
) | |
var mapOrder []string | |
var allMods []Mod | |
type Mod struct { | |
Name string | |
Path string | |
Poster string | |
ID string | |
Description string | |
Pack string | |
} | |
func main() { | |
modOrderString := "Basements;RV_Interior_Vanilla;RV_Interior_MP;ModManager;ModManagerELO;Factory;SnakeMansion;MilitaryComplex;Barco Abandonado;Riverside Gunstore;safehouse;Test;RfMCtBF_addon;MonmouthCounty_new;Nekos_Connection_Road_FK-EC+St.BH;FortKnoxLinked;EerieCountry;FortKnoxRoad;NewTersh;BedfordFalls;BillionaireSafehouse;Rivershore;OtrSR;CorOTRroad;Otr;KingsmouthKY;CigaroHouse;tikitown;LeavensburgCoreydonConnector;PortCityKYAbisimod;BridgeToCoryerdon;coryerdon;Lighthousematrioshka;Survival Farm;WPEFIX;WestPointExpansion;WestPointTrailerParkAndVhsStore;West Point Fire Department;The Frigate;Irvington_Rd;Irvington_KY;MRE;Waterlocked Pharmaceutical Factory;CONRTF;Speck_Map;RiversidemansionBrang;Riverside Fire Department;Jasperville;LeavenburgRiversideBridge;Leavenburg;catball_eastriverside;BBL;Bendys Bunker v2;DeltaCreekMunitions;Ztardew;ForestHouse;River_Homestead;Walnut_ridge;LCv2;Refordville;Winchester;Elliot Pond;Muldraugh-Westoutskirts ShippingCo;XRoadsGunExpo;NiceSurvivalist;AddamsMansion;Ashenwood;Elysium_Island;BigSurvivalist;Battlefield_Louisville_Stadium;Blackwood;Blueberry;Breakpoint;CampBusyBeaver_FortKickass;CedarHill;Chernaville;CherokeeLake;GarageLaZona;Crossroads Checkpoint;Cruise boat;EVAC_Louisville;EVAC_Muldraugh;EdsAutoSalvage;LyzzExotics;ForestRangerHideaway01;Fort_Boonesborough;FORTREDSTONE;Fort Rock Ridge;Fort Waterfront;Greenleaf;Heavens Hill;Hilltop;Myhometown;Hyrule County;lakeivytownship;Lande Desolate Camping;Lalafell's Heart Lake Town;Little Aoi's safe house2;LittleTownship;Louisville_Quarantine_Zone;BunkerDayOfTheDead;Louisville_Riverboat;Militaryairport;MuldraughCheckpoint;Muldraugh Fire Department;Nettle Township;NewEkron;NWBlockade;ParkingLot;Orchidwood(official version);OverlookHotel;Papaville;Peles_mansion;Pitstop;Portland;RabbitHashKY;RavenCreek;RedRacer;RemusMapMod;rbr;ReststopLouisville;pz_rosewoodexp_map;RMH;RosewoodVHSGunStores;cryocompound;Shortrest Mapjam Version;Southwood2.0;spiffosshelter;Springwood1;SPH;SuddenValleyHome;TeraMart - East Side;TheCompound;TheEyeLake;TheMallSouthMuldraughFIX;The Yacht;the_oasis;Pidgetown;Canvasback Studios;Louisville_River_Marina;SimonMDLVInternationalAirport;TheMuseumID;TrimbleCountyPowerStation;Utopia;Valhalla Community Safe Zone;WesternScrapCarYard;WeyhausenByCallnmx;wildberries;Xonics Mega Mall;Battlefield_Louisville_Hospital;DJBetsysFarm;nv_township_v1;Ranger'sHomestead;FlanHouse2.0baby;TWDterminus;ExpressTransferStation;LittleFarmstead;Barricaded Strip Mall Challenge;HLXEkronFarmhouse;Rosewood Mansion;railroadhouse;EkronmansionBrang;Daisy County;WestPointGatedCommunity;SimonMDConstructionSiteNoLoot;SimonMDRRRR;Fantasiado ST. Bernard's Hill;dylanstiles_bundle;tikitown_tiles;SkizotsTiles;OujinjinTiles;CustomMapBridge;Diederiks Tile Palooza;tkTiles_01;DylansTiles;PertsPartyTiles;melos_tiles_for_miles_pack;simonMDsTiles;FantaStreetTiles_01;EN_Newburbs;TryhonestyTiles;BigZombieMonkeys_tile_pack;DylansTiles_Elysium;EN_Flags;Cookie_Tiles;EN_Flags_Craft;hopewell_eng_orig;hopewell_eng_zombies;69camaro;82oshkoshM911;86oshkoshP19A;87fordB700;93mustangSSP;AquatsarYachtClub;tsarslib;ArmoredVests;Arsenal(26)GunFighter;modoptions;Arsenal(26)GunFighter[MAIN MOD 2.0];Authentic Z - Current;AuthenticZBackpacks+;AuthenticZLite;amclub;BetterSortCC;BB_Utils;BB_Bicycles;Brita_2;Brita;LastMinutePrepperReloaded;BCGRareWeapons;BCGTools;BB_CommonSense;OutTheWindow;OutTheWindowAnimSkizotsVisibleBoxesandGarbage2;Skizots Visible Boxes and Garbage2;DashRoamer;DashRoamerRVInterior;diveThroughWindows;DRAW_ON_MAP;EasyConfigChucked;EQUIPMENT_UI;ExpandedHelicopterEvents;swefpifh.fbioffice;ForagingZ;BetterContainers;SVR_GreenFire_Patch4176;jiggasGreenfireMod;GunFighter_Radial_Menu;P4HasBeenRead;RiskyInspectWeapon;KillCount;MinimalDisplayBars;ModTemplate;MonmouthCountyTributeLegacy;moodle_quarters;MoreDescriptionForTraits4166;MuldraughCheckpoint[HARDMODE];NewTershNorthConnectionRoad;NewTershSouthRoadToKnox;nattachments;noirrsling;PitstopLegacy;RainCleansBlood;ReducedWoodWeight2x41;rbrA2;rideabletrucks;ScrapArmor(new version);TheWorkshop(new version);ScrapGuns(new version);ScrapWeapons(new version);BLTAnnotations;SimpleOverhaulMeleeWeapons;SimpleOverhaulMeleeWeapons_EasySpearAttachments;SimplePlayablePianos4150;SleepWithFriends;TMC_TrueActions;TrueActionsDancing;TrueActionsDancingVHS;TrueActionsDancingVHS_MAG;UncleRedsBunkerRedux;TheStar;WorkingMasks;Zaan's_Community_Design;Zaan's_Scandinavian_Design;Zaan's Union Town;ArsenalOpenAmmoWalk;B41OpenAmmoWalk;addForceRespawnToCar;bounderRV_Kang;cherbourg;errorMagnifier;fuelsideindicator" | |
modsOrder := strings.Split(modOrderString, ";") | |
_ = modsOrder // placeholder | |
workShopPath := "G:/SteamLibrary/steamapps/workshop/content/108600" | |
fmt.Println("mod count:", len(modsOrder)) | |
// read in all mods loaded into the workshop folder | |
err := filepath.Walk(workShopPath, func(path string, info os.FileInfo, err error) error { | |
if err != nil { | |
return err | |
} | |
if info.IsDir() { | |
//log.Println("in mod dir:", path) | |
// if end of path is \mods | |
if strings.HasSuffix(path, "\\mods") { | |
// look recursively under path for mod.info | |
err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { | |
if err != nil { | |
return err | |
} | |
if info.Name() == "mod.info" { | |
mod, err := parseModInfoFile(path) | |
if err != nil { | |
log.Printf("error parsing mod.info file: %v", err) | |
return nil | |
} | |
mod.Path = strings.TrimRight(path, "mod.info") | |
allMods = append(allMods, *mod) | |
} | |
return nil | |
}) | |
} | |
} | |
return nil | |
}) | |
if err != nil { | |
fmt.Println(err) | |
} | |
log.Println("loaded mods:", len(allMods)) | |
// deduplicate allMods, because some mods have multiple subfolders, so just keep one parent folder | |
// can actually still read all mods under parent folder with another walk anyway | |
log.Println("deduplicating") | |
deduplicatedMods := make([]Mod, 0, len(allMods)) | |
modMap := make(map[string]bool) | |
for _, mod := range allMods { | |
if !modMap[mod.ID] { | |
deduplicatedMods = append(deduplicatedMods, mod) | |
modMap[mod.ID] = true | |
} | |
} | |
allMods = deduplicatedMods | |
log.Println("deduped to:", len(allMods)) | |
// create new slice of mods by ID ordered using the order defined in modsOrder | |
// this is to read in the map list in the same order as the original mod order | |
var ordered []Mod | |
for _, modID := range modsOrder { | |
for _, mod := range allMods { | |
if mod.ID == modID { | |
ordered = append(ordered, mod) | |
break | |
} | |
} | |
} | |
// iterate through each mod in ordered, and do filepath.Walk to look for all map.info files | |
var maps int // map mod count | |
for _, om := range ordered { | |
err := filepath.Walk(om.Path, func(path string, info os.FileInfo, err error) error { | |
if err != nil { | |
return err | |
} | |
// if this path has a map.info | |
if strings.Contains(path, "map.info") { | |
fmt.Printf("mod: %s has a map named: '%s' in: %s\n", om.Name, getMapName(path), om.Path) | |
mapOrder = append(mapOrder, getMapName(path)) | |
maps++ | |
} | |
return nil | |
}) | |
if err != nil { | |
fmt.Println(err) | |
} | |
} | |
fmt.Println("maps:", mapOrder) | |
// create&print output string | |
var output bytes.Buffer | |
for _, mmm := range mapOrder { | |
output.WriteString(fmt.Sprintf("%s;", mmm)) | |
} | |
output.WriteString("Muldraugh, KY") | |
fmt.Println(output.String()) | |
// Write the output to 'maps.txt' | |
err = os.WriteFile("maps.txt", []byte(output.String()), 0644) | |
if err != nil { | |
fmt.Println("There was an error writing to the file:", err) | |
} | |
log.Println("wrote map order to file") | |
} | |
func getMapName(path string) string { | |
// example string: | |
// \steamapps\workshop\content\108600\1843248433\mods\Forest House\media\maps\A house in the woods\map.info | |
return filepath.Base(filepath.Dir(path)) | |
} | |
func parseModInfoFile(filename string) (*Mod, error) { | |
file, err := os.Open(filename) | |
if err != nil { | |
return nil, err | |
} | |
defer file.Close() | |
mod := &Mod{} | |
scanner := bufio.NewScanner(file) | |
for scanner.Scan() { | |
line := scanner.Text() | |
kv := strings.SplitN(line, "=", 2) | |
if len(kv) != 2 { | |
//log.Printf("invalid line: %s", line) | |
continue | |
} | |
key, value := kv[0], kv[1] | |
// Assign values to Mod fields | |
switch key { | |
case "name": | |
mod.Name = value | |
case "poster": | |
mod.Poster = value | |
case "id": | |
mod.ID = value | |
case "description": | |
mod.Description = value | |
case "pack": | |
mod.Pack = value | |
} | |
} | |
if err := scanner.Err(); err != nil { | |
return nil, err | |
} | |
return mod, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment