Skip to content

Instantly share code, notes, and snippets.

@json-m
Last active January 18, 2024 01:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save json-m/66bccaafd64e5ac75966d0698a336277 to your computer and use it in GitHub Desktop.
Save json-m/66bccaafd64e5ac75966d0698a336277 to your computer and use it in GitHub Desktop.
project zomboid dedicated server map load order
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