Skip to main content
atlantikaryan
Up and Coming
November 30, 2025
Solved

Player Persistent Variables not saving from Published World (works in Editor)

  • November 30, 2025
  • 7 replies
  • 263 views

Not sure if this is the best place to post this, but we are running into an issue with our player persistent variables not saving data between sessions while a player is in our recently Published world. We have confirmed the following:

  • Saving and loading works fine in the Editor (and has for months), including between sessions.
  • Saving something from an Editor for a player does successfully get loaded by that player in a Published session.
  • Nothing is being saved from the Published world back to our player persistent variables.

Has anyone else encountered an issue like this? Or is there potentially a configuration step we missed when publishing?

Best answer by gausroth

The only way I know of that will allow you to see the logs in a published world is the Debug Console gizmo. Just make sure it is set to Published mode. Then any editor of the world should be able to see the gizmo in published mode.

7 replies

gausroth
Honored Contributor
November 30, 2025

Can you share the code for this?

Published mode runs faster than the editor. It could be a race condition. If you're doing it when the player leaves the world you only have the one frame to do so, or the player reference will be null.

GitHub: https://github.com/Gausroth/HorizonWorldsTutorials.git Discord: https://discord.gg/hNyDQwdw4N YouTube: https://www.youtube.com/@GausRothTutorials
stealthbazatlantika
New Participant
November 30, 2025

We do perform a save when we onExitWorld, but we also call it at certain points when we grant items. (we are using a playerVar as our persistence layer)

We have alot going on.  But suffice it to say that we call "SetPlayervars" in the path both during the test, AND on exit.


The interesting part to me is why does it work fine reading/ writing in editor.. and reading seems fine in the published world. (if you go do something in editor.. then load that same player in the published world on device.. you have your data as expected)   But saving just doesn't seem to happen.

Is there anywhere we can go to look for error logs or anything for the published world ?





Here is a  bunch of our code for context:

export type PlayerVar = {
  version: number;
  name: string;
  visits: number;
  usingUI: boolean;
  inventory: Array<{itemId: string, quantity: number}>;
  equippedBowItemId: string;  //DEPRECATED
  equippedArtifactItemId: string;
  equippedWingsItemId: string;
  playerXP: number;
  equippedPetItemId: string;
  completedObjectiveIDs: string[];
}

export const allPlayerVarData = new Map<Player, PlayerVar>();


const playerVarName = `PlayerVarsObj`;
const varGroupName: string = "PlayerData_Live"; //group name for variable group
const varKey: string = `${varGroupName}:${playerVarName}`;


preStart() {
    if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER)
      console.log(`PlayerDataManager: Prestart PlayerDataManager`);
    
    this.connectCodeBlockEvent(this.entity, CodeBlockEvents.OnPlayerEnterWorld, this.playerEnterWorld.bind(this));
     this.connectCodeBlockEvent(this.entity, CodeBlockEvents.OnPlayerExitWorld, this.playerExitWorld.bind(this));   
  }

  start(){    
    if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER)
      console.log("PlayerDataManager: PlayerDataManager started");
    
  }
  
  playerEnterWorld(player: Player) {
    
    let newPlayerVar = this.GetPlayerVars(player);
    
    if (newPlayerVar == null){
      console.error(`PlayerDataManager: newPlayerVar is null after calling GetPlayerVar`);
      return;
    }

    newPlayerVar.visits++;

    allPlayerVarData.set(player, newPlayerVar);
    
    if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER)
      console.log(`PlayerDataManager: Player ${player.name} has entered the world. and visited ${newPlayerVar.visits} times.`);  

  }

  playerExitWorld(player: Player){

    const playerVar = allPlayerVarData.get(player);

    if (playerVar){
      this.world.persistentStorage.setPlayerVariable(player, varKey, playerVar);
    }
    else {
      if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER)
        console.log(`PlayerDataManager: playerVar returned undefined on PlayerVarManager: playerExitWorld`);
    }

    allPlayerVarData.delete(player);

    if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER)
      console.log(`PlayerDataManager: Player ${player.name} has exited the world.`);
  }



  // A method to get the player variable for external scripts
  public GetPlayerVars(player: Player): PlayerVar | null{
    let prevPlayerVar : PlayerVar | null = null;    

    const valueJson : string | null = this.world.persistentStorage.getPlayerVariable(player, varKey);

    if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER)
      console.log(`PlayerDataManager: valueJSON for ${player.name} is: ${valueJson}`);      

    prevPlayerVar = valueJson as unknown as PlayerVar;
    
    if (prevPlayerVar == null || prevPlayerVar === undefined || prevPlayerVar.version == null || prevPlayerVar.version === undefined){
        if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER)
          console.log(`PlayerDataManager: prevPlayerVar is null or undefined, or its version is null or undefined after retrieving from storage and converting`);
    }

    const newPlayerVar = initializeOrMigratePlayerVars(player, prevPlayerVar);
    
    return newPlayerVar;
  }

  //A method to set ALL the player vars via a "player var" object.
  public SetPlayerVars(player: Player, playerVar: PlayerVar){
    
    if (playerVar){
      this.world.persistentStorage.setPlayerVariable(player, varKey, playerVar);      
      this.sendLocalBroadcastEvent(Events.onPlayerDataChanged, { player: player});
    }
    else {
      console.log(`PlayerDataManager: playerVar returned undefined on PlayerVarManager: playerExitWorld`);
    }
  }










//This method will check an existing playerVar object, and migrate it to the latest version if needed
//If no previous object is found, it will initialize a new one
function initializeOrMigratePlayerVars(player: Player, prevPlayerVar: PlayerVar | null): PlayerVar {
  
  //Init a new player variable
  const newPlayerVar: PlayerVar = {
    version: playerVariableVersion,
    name: player.name.get(), //v1
    visits: 1, //v1
    usingUI: true, //v2
    inventory: PlayerDataManager.convertInventoryItemEntries(STARTING_INVENTORY), //v3
    equippedBowItemId: "" , //v4 //DEPRECATED
    equippedWingsItemId: "" , //v4
    equippedPetItemId: "" , //v4
    equippedArtifactItemId: "" , //v5
    playerXP: 0, //v6,
    completedObjectiveIDs: [] //v7
  }  

  if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
    console.log(`PlayerDataManager: initializeOrMigratePlayerVars PrevPlayerVar Object: ${JSON.stringify(prevPlayerVar)}`);
  }


  // Check if the player has a previous variable
  // If so migrate the data
  if (prevPlayerVar != null && prevPlayerVar !== undefined && prevPlayerVar.version != null){
    
    if (prevPlayerVar.version < playerVariableVersion){
      //PERFORM MIGRATION
      
      // Carry over existing data from 1st version
      newPlayerVar.visits = prevPlayerVar.visits;
      newPlayerVar.name = prevPlayerVar.name;

      // Migrate to version 2
      if (prevPlayerVar.version === 1){
        newPlayerVar.usingUI = false; // New field in version 2, default to false
      }
      else{
        newPlayerVar.usingUI = prevPlayerVar.usingUI;
      }

      // Migrate to version 3
      if (prevPlayerVar.version < 3){
        if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
          console.log(`PlayerDataManager: Migrating player inventory to version 3 for player ${player.name}`);
        }
        newPlayerVar.inventory = PlayerDataManager.convertInventoryItemEntries(STARTING_INVENTORY);
      }
      else{
        if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
          console.log(`PlayerDataManager: Carrying over existing inventory for player ${player.name}`);
        }
        newPlayerVar.inventory = prevPlayerVar.inventory;
      }

      // Migrate to version 4
      if (prevPlayerVar.version < 4){
        if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
          console.log(`PlayerDataManager: Initializing equipped items to empty for player ${player.name}`);
        }
        newPlayerVar.equippedArtifactItemId = "" ;
        newPlayerVar.equippedWingsItemId = "" ;
        newPlayerVar.equippedPetItemId = "" ;
      }
      else{
        // No migration needed, use existing data
        newPlayerVar.equippedArtifactItemId = prevPlayerVar.equippedArtifactItemId;
        newPlayerVar.equippedWingsItemId = prevPlayerVar.equippedWingsItemId;
        newPlayerVar.equippedPetItemId = prevPlayerVar.equippedPetItemId;        
      }


      // Migrate to version 5
      if (prevPlayerVar.version < 5){
        if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
          console.log(`PlayerDataManager: Initializing equipped artifact item to empty for player ${player.name}`);
        }
        newPlayerVar.equippedArtifactItemId = "" ;
        if (prevPlayerVar.equippedBowItemId != null && prevPlayerVar.equippedBowItemId !== undefined && prevPlayerVar.equippedBowItemId !== ""){
          // Carry over the equipped bow item to the new artifact slot
          newPlayerVar.equippedArtifactItemId = prevPlayerVar.equippedBowItemId;
          if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
            console.log(`PlayerDataManager: Migrated equipped bow item ${prevPlayerVar.equippedBowItemId} to equipped artifact slot for player ${player.name}`);
          }
        }
      }
      else{
        // No migration needed, use existing data
        newPlayerVar.equippedArtifactItemId = prevPlayerVar.equippedArtifactItemId;        
      }

      // Migrate to version 6
      if (prevPlayerVar.version < 6){
        if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
          console.log(`PlayerDataManager: playerXP for player ${player.name}`);
        }
        newPlayerVar.playerXP = 0;        
      }
      else{
        // No migration needed, use existing data
        newPlayerVar.playerXP = prevPlayerVar.playerXP;        
      }

      // Migrate to version 7
      if (prevPlayerVar.version < 7){
        if (DISPLAY_CONSOLE_PLAYER_DATA_MANAGER){
          console.log(`PlayerDataManager: completedObjectiveIDs for player ${player.name}`);
        }
        newPlayerVar.completedObjectiveIDs = [];
      }
      else{
        // No migration needed, use existing data
        newPlayerVar.completedObjectiveIDs = prevPlayerVar.completedObjectiveIDs;
      }
    }
    else{
      //no migration needed at all.
      return prevPlayerVar;
    }
    
  }

  // the new player var has been setup and migrated if needed
  //return it!
  return newPlayerVar;
}





gausroth
gausrothAnswer
Honored Contributor
December 1, 2025

The only way I know of that will allow you to see the logs in a published world is the Debug Console gizmo. Just make sure it is set to Published mode. Then any editor of the world should be able to see the gizmo in published mode.

GitHub: https://github.com/Gausroth/HorizonWorldsTutorials.git Discord: https://discord.gg/hNyDQwdw4N YouTube: https://www.youtube.com/@GausRothTutorials
SeeingBlue
Honored Contributor
December 1, 2025

Did AI generate some of this? What is going on with this line:

prevPlayerVar = valueJson as unknown as PlayerVar;


I don't see anything immediately wrong, but I would make sure my PlayerVar value is correct before I try to save it by printing it to a console.log.

Upcoming Events : Performance Pro - UI Unleashed - Building Responsive & Performant Interfaces Thursday June 19th 5:00 PM PT - Register June Programming Calendar
gausroth
Honored Contributor
December 1, 2025

SeeingBlue​  Kinda looks like the one Laex05 did but with some modifications. stealthbazatlantika​ have you looked at this? https://developers.meta.com/horizon-worlds/learn/documentation/mhcp-program/community-tutorials/meta-horizon-typescript-api-20-json-ppvs--versioning-

 

GitHub: https://github.com/Gausroth/HorizonWorldsTutorials.git Discord: https://discord.gg/hNyDQwdw4N YouTube: https://www.youtube.com/@GausRothTutorials
atlantikaryan
Up and Coming
December 2, 2025

Thanks gausroth​ and SeeingBlue​ for your help with this. Gausroth's tip about running the Debug Console gizmo led us to realizing that there was a Player OnExitWorld() call that wasn't happening in the Editor (for whatever reason) which was overwriting the player's progress for a session on the Published world.

After sorting that out we were able to get things working properly again!

gausroth
Honored Contributor
December 3, 2025

OnExitWorld() never gets called in the editor. It might when you return to the creation menu but not sure.

GitHub: https://github.com/Gausroth/HorizonWorldsTutorials.git Discord: https://discord.gg/hNyDQwdw4N YouTube: https://www.youtube.com/@GausRothTutorials