• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!
  • Hello [name]! Thanks for joining the GMC. Before making any posts in the Tech Support forum, can we suggest you read the forum rules? These are simple guidelines that we ask you to follow so that you can get the best help possible for your issue.

Android Android ds_map / save issues

siread

Member
I have an game on the iOS and Android stores called Retro Bowl and I have a small number of Android users experiencing a problem which looks like some team data has corrupted.

upload_2020-2-11_11-53-40.png

All the teams in the game are ds_maps and they are stored in an ini file:

Code:
    // Store team data
    var size = ds_list_size(teamlist);
    ini_write_real("savegame", "teamlist", size);

    for (var m = 0; m < size; m++)
    {
        var str = ds_map_write(ds_list_find_value(teamlist, m));
        ini_write_string("savegame", "teamlist_"+string(m), str);
    }
Code:
    // Read team data
    size = ini_read_real("savegame", "teamlist", 0);
    slog("teamlist size="+string(size));

    for (var m = 0; m < size; m++)
    {
        var map = ds_map_create();
        ds_map_read(map, ini_read_string("savegame", "teamlist_"+string(m), ""));
        ds_list_add(teamlist, map);
    }
This works fine but a handful of players have suffered issues where team data has gone missing...

I don't think it is necessarily the save/load system at fault, the only reason I'm am looking at that is because I'm wondering if it can be flakey on some Android devices (file size is about 500kb). It would be more likely that somewhere along the line the ds_map for a particular team is getting lost, but I cannot fathom why it would only affect Android users (30k installs compared to 180k on iOS). There are some sorting functions that rearrange team map order, but I never mess with the master list, merely newly created lists that reference the original "teamlist" maps. And again, why would this only occur on Android?

Any ideas will be greatly appreciated!

Also, is there any way to get access to a users save file? Would they need to have a rooted device?
 

Attachments

Last edited:

FrostyCat

Redemption Seeker
It's probably an issue with either the INI functions mishandling certain "taboo" characters in the output of ds_map_write() (example), or ds_map_write() and ds_map_read() not lining up on the Android export only. The flakiness of INI functions and the opaqueness of data structure read/write functions have turned me off both, and I now use JSON exclusively.

If you have physical access to the device, you can check your app's files using ADB as described on this StackOverflow topic. If you don't, you have to set up an upload service to receive the file online over HTTPS.
 

siread

Member
The team data itself doesn't contain any unusual characters (it's just city names and numbers) but it's a lead at least, thanks.
 

siread

Member
I'm still struggling with this issue. It seems to be a case of players quitting out of the app mid-game or possibly getting interrupted by a phone call or message, then when they return to the game (restarting the app) some data has corrupted in the save file. The game never saves until the game action has completed (which is when players are force quitting or getting interrupted) and this error never occurs on iOS so it is presumably something to do with how Android handles save data. I just can't understand how anything is getting corrupted if the save_game() script is not getting called. Any ideas?
 
Last edited:

Ricardo

Member
Idea: Where's ini_open/close in your code? Make sure the save file is being correctly opened (and closed) and that it doesn't remain open while the game is running.
 

siread

Member
I like your thinking but I cannot see how the file could stay open. I only call ini_open in two scripts (loading/saving) and ini_close always takes place at the end of it.

GML:
    ini_open(g_savename);
  
    ini_write_real("savegame", "year", year);
    ini_write_real("savegame", "week", week);
    ini_write_real("savegame", "stage", stage);
    ini_write_real("savegame", "intro", intro);
    ini_write_string("savegame", "fname", fname);
    ini_write_string("savegame", "lname", lname);
    ini_write_real("savegame", "myteam_id", myteam_id);
    ini_write_real("savegame", "myface_x", myface_x);
    ini_write_real("savegame", "myface_y", myface_y);
    ini_write_real("savegame", "favourite_team_id", favourite_team_id);
    ini_write_real("savegame", "suppress_difficulty", suppress_difficulty);
    ini_write_real("savegame", "salary_cap", salary_cap);
    ini_write_real("savegame", "prematch_dilemma_done", prematch_dilemma_done);
    ini_write_real("savegame", "tip_count", tip_count);
    ini_write_real("savegame", "matchcount", matchcount);
  
    ini_write_real("savegame", "coach_credit", coach_credit);
    ini_write_real("savegame", "coach_rating", coach_rating);
    ini_write_real("savegame", "fans", fans);
    ini_write_real("savegame", "facility_stadium", facility_stadium);
    ini_write_real("savegame", "facility_training", facility_training);
    ini_write_real("savegame", "facility_rehab", facility_rehab);
    ini_write_real("savegame", "facility_upgraded_stadium", facility_upgraded_stadium);
    ini_write_real("savegame", "facility_upgraded_training", facility_upgraded_training);
    ini_write_real("savegame", "facility_upgraded_rehab", facility_upgraded_rehab);
  
    ini_write_real("savegame", "draft_round", draft_round);
    ini_write_real("savegame", "draft_picks_0", draft_picks[0]);
    ini_write_real("savegame", "draft_picks_1", draft_picks[1]);
    ini_write_real("savegame", "draft_picks_2", draft_picks[2]);
    ini_write_real("savegame", "draft_info_done", draft_info_done);
    ini_write_real("savegame", "expiredcontract_msg_done", expiredcontract_msg_done);
    ini_write_real("savegame", "offers_done", offers_done);
  
    // Stats
    for (var i = 0; i < 3; i++)
    {
        ini_write_real("savegame", "stat_games"+string(i), stat_games[i]);
        ini_write_real("savegame", "stat_comp"+string(i), stat_comp[i]);
        ini_write_real("savegame", "stat_att"+string(i), stat_att[i]);
        ini_write_real("savegame", "stat_yds"+string(i), stat_yds[i]);
        ini_write_real("savegame", "stat_td"+string(i), stat_td[i]);
        ini_write_real("savegame", "stat_int"+string(i), stat_int[i]);
        ini_write_real("savegame", "stat_sck"+string(i), stat_sck[i]);
    }

    // Store News
    ini_write_string("savegame", "news", ds_list_write(newslist));
  
    // Store Dilemmas
    ini_write_string("savegame", "dilemma_tags", ds_list_write(dilemma_tags));
  
    // Store teams
    var size = ds_list_size(teamlist);
    ini_write_real("savegame", "teamlist", size);

    for (var m = 0; m < size; m++)
    {
        var str = ds_map_write(ds_list_find_value(teamlist, m));
        ini_write_string("savegame", "teamlist_"+string(m), str);
    }

    // Store achievements
    var size = ds_list_size(achievements);
    ini_write_real("savegame", "achievements", size);

    for (var m = 0; m < size; m++)
    {
        var val = ds_list_find_value(achievements, m);
        ini_write_string("savegame", "achievements_"+string(m), ds_map_write(val));
    }
  
    // Store history
    var size = ds_list_size(history);
    ini_write_real("savegame", "history", size);

    for (var m = 0; m < size; m++)
    {
        var str = ds_map_write(ds_list_find_value(history, m));
        ini_write_string("savegame", "history_"+string(m), str);
    }
  
    // Store schedule
    size = ds_list_size(schedule);
    ini_write_real("savegame", "schedule", size);

    for (var m = 0; m < size; m++)
    {
        var str = ds_map_write(ds_list_find_value(schedule, m));
        ini_write_string("savegame", "schedule_"+string(m), str);
    }

    // Store fixtures
    size = ds_list_size(fixturelist);
    ini_write_real("savegame", "fixturelist", size);

    for (var m = 0; m < size; m++)
    {
        var str = ds_map_write(ds_list_find_value(fixturelist, m));
        ini_write_string("savegame", "fixture_"+string(m), str);
    }

    // Store playoff data
    size = ds_list_size(playoffs);
    ini_write_real("savegame", "playoffs", size);

    for (var m = 0; m < size; m++)
    {
        var str = ds_map_write(ds_list_find_value(playoffs, m));
        ini_write_string("savegame", "playoff_"+string(m), str);
    }
  
    // Store draftlist
    size = ds_list_size(draftlist);
    ini_write_real("savegame", "draftlist", size);

    for (var r = 0; r < size; r++)
    {
        var str = ds_map_write(ds_list_find_value(draftlist, r));
        ini_write_string("savegame", "draftlist_"+string(r), str);
    }
  
    // Store hirelist
    size = ds_list_size(hirelist);
    ini_write_real("savegame", "hirelist", size);

    for (var r = 0; r < size; r++)
    {
        var str = ds_map_write(ds_list_find_value(hirelist, r));
        ini_write_string("savegame", "hirelist_"+string(r), str);
    }
  
    // Store storelist
    size = ds_list_size(storelist);
    ini_write_real("savegame", "storelist", size);

    for (var r = 0; r < size; r++)
    {
        var str = ds_map_write(ds_list_find_value(storelist, r));
        ini_write_string("savegame", "storelist_"+string(r), str);
    }
  
    // Store trade list
    size = ds_list_size(tradelist);
    ini_write_real("savegame", "tradelist", size);

    for (var r = 0; r < size; r++)
    {
        var str = ds_map_write(ds_list_find_value(tradelist, r));
        ini_write_string("savegame", "tradelist_"+string(r), str);
    }
  
    // Store free agents
    size = ds_list_size(freeagentlist);
    ini_write_real("savegame", "freeagentlist", size);

    for (var r = 0; r < size; r++)
    {
        var str = ds_map_write(ds_list_find_value(freeagentlist, r));
        ini_write_string("savegame", "freeagentlist_"+string(r), str);
    }
  
    // Store Roster
    size = ds_list_size(roster);
    ini_write_real("savegame", "roster", size);

    for (var r = 0; r < size; r++)
    {
        var str = ds_map_write(ds_list_find_value(roster, r));
        ini_write_string("savegame", "roster_"+string(r), str);
    }
  
    // Store stafflist
    size = ds_list_size(stafflist);
    ini_write_real("savegame", "stafflist", size);

    for (var r = 0; r < size; r++)
    {
        var str = ds_map_write(ds_list_find_value(stafflist, r));
        ini_write_string("savegame", "stafflist_"+string(r), str);
    }
  
    // Face grid
    ini_write_string("savegame", "face_grid_L", ds_grid_write(face_grid_L));
    ini_write_string("savegame", "face_grid_M", ds_grid_write(face_grid_M));
    ini_write_string("savegame", "face_grid_D", ds_grid_write(face_grid_D));
    ini_write_string("savegame", "face_grid_C", ds_grid_write(face_grid_C));
  
    // Close
    ini_close();
 

Ricardo

Member
wow, you're saving A LOT of data in these INI files. INI are usually used for small configuration snippets, and as other had already mentioned, it is not good when storing too much. I highly suggest you avoid INI storage in future projects if you need to store big chunks of data. Ideally you'll use the async buffer functions, but json_encode/decode will also work like a charm for you since you're already using a lot of ds_maps to store all your data.

Back to the original problem: it is really hard to give advice on this specific case. I think that your best chance of figuring out what's going on is to try to replicate the issue by yourself. Try to interrupt the game by calling to your phone while playing, force quitting and doing other unexpected things. I you manage to replicate the issue locally, then you can use GMS2 debugger to track down exactly what's going on.

Otherwise it is very unlikely someone will be able to give you better advice.

best of luck!
 

siread

Member
The save files have grown over the course of the project but I didn't know that they were only suitable for saving small files - they work fine in general and perfectly on iOS. I'll look into the async stuff, thanks.
 

Ricardo

Member
According to the docs, INI files support up to 64KB. You mentioned your files are about 500KB, and also that they grow in size over time. So, yeah... Definitely not a good solution for your needs. Considering the amount of data you are throwing there, I'm actually quite surprised you didn't encounter issues earlier.
Anyways. It seems like you are storing your data in ds_maps already, so take a look at ds_map_add_map and at the json functions. You could add all your maps into a central map, json encode it and then use a text file function to write it to a file. I'm sure that'll work a lot better than INI.
 

Amon

Member
Hi siread. It's nice to see an ex blitzer here. :)

JSON is definitely the way to go here. I had similar issues with large data saves using ini files and JSON saved the day.
 

siread

Member
Hey Amon - good to see you!

It's weird that the docs mention the 64kb limit when it's clearly not a hard and fast rule. The saves work fine on PC, Mac, iOS, even html5, and less than 0.1% of Android users have had an issue which makes me wonder if it's something else entirely. Why would it have a limit when it's just a text file at the end of the day? Anyway, I'd best get my head down and start converting the save system!
 

Nocturne

Friendly Tyrant
Forum Staff
Admin
It's weird that the docs mention the 64kb limit when it's clearly not a hard and fast rule. The saves work fine on PC, Mac, iOS, even html5, and less than 0.1% of Android users have had an issue which makes me wonder if it's something else entirely. Why would it have a limit when it's just a text file at the end of the day? Anyway, I'd best get my head down and start converting the save system!
It's NOT a hard and fast rule now, but it is certainly something that USED to be and it can still be on some OS/devices... I had many many headaches trying to save out datastructures to ini files when I started using GM and came up against this very issue a lot, and now only use ini's for saving game options and minor data like that.
 
Top