// achievement tracking Class SWWMAchievementInfo { int baseindex; // sorting order in the list String basename; // base name for identifying achievement TextureID icon; // our icon int maxval; // maximum value (0: no progress) bool hasformat; // achievement description text must be formatted to add maxval bool bitfield; // progress is tracked as a bitfield int state, val; // set by other scripts, used to avoid excessive dictionary lookups } extend Class SWWMStaticHandler { ui int lastachievementnotify; // prevent overlap Dictionary achievementstate, achievementprogress; Array achievementinfo; // specific info on achievements private ui bool CheckAchievement( SWWMAchievementInfo a ) { int val = achievementstate.At(a.basename).ToInt(); // manually check progress if ( a.maxval ) { int prog = achievementprogress.At(a.basename).ToInt(); // special case for bitfields if ( a.bitfield ) { int pb = 0; for ( int i=0; i= a.maxval) ) { achievementstate.Insert(a.basename,"1"); val = 1; } } else { if ( val && (prog < a.maxval) ) { achievementstate.Insert(a.basename,"0"); val = 0; } else if ( !val && (prog >= a.maxval) ) { achievementstate.Insert(a.basename,"1"); val = 1; } } } if ( (val == 1) && (gametic > lastachievementnotify) ) { achievementstate.Insert(a.basename,"2"); EventHandler.SendNetworkEvent("swwmachievement."..a.basename,consoleplayer); let notif = new("SWWMAchievementNotification").Init(a.basename,a.icon,a.hasformat?a.maxval:0); StatusBar.AttachMessage(notif,-3478); lastachievementnotify = gametic+200; } return (val > 0); } private ui void CheckAllAchievements() { // don't check constantly, and don't check during level transitions if ( unloading || (maptime < 105) || (maptime%35) ) return; bool alldone = true; int ev = -1; for ( int i=0; i lastachievementnotify) ) { achievementstate.Insert("everything","2"); EventHandler.SendNetworkEvent("swwmachievement."..achievementinfo[ev].basename,consoleplayer); let notif = new("SWWMAchievementNotification").Init(achievementinfo[ev].basename,achievementinfo[ev].icon); StatusBar.AttachMessage(notif,-3478); lastachievementnotify = gametic+200; } } // parses achievements.lst file(s) private void ParseAchievementList( out Array achievements ) { achievements.Clear(); let lmp = Wads.FindLumpFullName("achievements.lst"); if ( lmp == -1 ) ThrowAbortException("'achievements.lst' not found"); String dat; Array list, ln; int bidx = 0; while ( lmp != -1 ) { dat = Wads.ReadLump(lmp); // Windows pls dat.Replace("\r",""); list.Clear(); dat.Split(list,"\n"); foreach ( l:list ) { if ( (l.Length() == 0) || (l.Left(1) == "#") || (l.Left(1) == "") ) continue; ln.Clear(); l.Split(ln,",",0); // game filtering if ( !(gameinfo.gametype&GAME_DOOM) && (ln[3] ~== "doom") ) continue; else if ( !(gameinfo.gametype&GAME_HERETIC) && (ln[3] ~== "heretic") ) continue; else if ( !(gameinfo.gametype&GAME_HEXEN) && (ln[3] ~== "hexen") ) continue; else if ( !(gameinfo.gametype&GAME_RAVEN) && (ln[3] ~== "raven") ) continue; else if ( !(gameinfo.gametype&(GAME_DOOM|GAME_HERETIC)) && (ln[3] ~== "nothexen") ) continue; let ac = new("SWWMAchievementInfo"); ac.baseindex = bidx; ac.basename = ln[0]; ac.icon = TexMan.CheckForTexture("graphics/Achievements/Achievement"..ac.basename..".png"); // fallback icon if one is not found if ( !ac.icon.IsValid() ) ac.icon = TexMan.CheckForTexture("graphics/Achievements/DefaultAchievement.png"); ac.maxval = ln[1].ToInt(); // special cases for maxval == -1 (currently only one, so this is simplified) if ( (ac.maxval == -1) && (ac.basename == "allcoll") ) { int nc = 0; foreach ( cls:AllActorClasses ) { let c = (Class)(cls); if ( !c || (c == 'SWWMCollectible') ) continue; let def = GetDefaultByType(c); // check that we can collect it in this IWAD if ( !def.ValidGame() ) continue; nc++; } ac.maxval = nc; } // bitfield else if ( ac.maxval < -1 ) { ac.bitfield = true; ac.maxval = abs(ac.maxval); } ac.hasformat = (ln[2]~=="yes"); achievements.Push(ac); bidx++; } lmp = Wads.FindLumpFullName("achievements.lst",lmp+1); } } // load all achievement state and progress private void LoadAchievements() { String statestr = swwm_achievementstate; String progstr = swwm_achievementprogress; // invalid length? if ( (statestr.length()%2) || (progstr.length()%2) ) { CreateAchievements(); return; } // decode static const int cypher[] = {0xAD,0xEA,0xDB,0xED}; String nstr = ""; for ( uint i=0; i keys; keys.Clear(); statestr.Split(keys,","); foreach ( k:keys ) { int colon = k.IndexOf(":"); // no value separator? if ( colon == -1 ) { CreateAchievements(); return; } achievementstate.Insert(k.Left(colon),k.Mid(colon+1)); } keys.Clear(); progstr.Split(keys,","); foreach ( k:keys ) { int colon = k.IndexOf(":"); // no value separator? if ( colon == -1 ) { CreateAchievements(); return; } achievementprogress.Insert(k.Left(colon),k.Mid(colon+1)); } // load achievement info and trim any bogus keys, as well as adding any new ones that are missing ParseAchievementList(achievementinfo); let di = DictionaryIterator.Create(achievementstate); while ( di.Next() ) { String key = di.Key(); bool deleteme = true; foreach ( inf:achievementinfo ) { if ( inf.basename != key ) continue; deleteme = false; break; } if ( deleteme ) { if ( developer >= 2 ) Console.Printf("Deleting bogus achievement state %s = %s",key,di.Value()); achievementstate.Remove(key); } } di = DictionaryIterator.Create(achievementprogress); while ( di.Next() ) { String key = di.Key(); bool deleteme = true; foreach ( inf:achievementinfo ) { if ( inf.basename != key ) continue; if ( !inf.maxval ) continue; deleteme = false; break; } if ( deleteme ) { if ( developer >= 2 ) Console.Printf("Deleting bogus achievement progress %s = %s",key,di.Value()); achievementprogress.Remove(key); } } foreach ( inf:achievementinfo ) { if ( achievementstate.At(inf.basename) == "" ) { if ( developer >= 2 ) Console.Printf("Adding missing achievement state %s",inf.basename); achievementstate.Insert(inf.basename,"0"); } if ( inf.maxval && (achievementprogress.At(inf.basename) == "") ) { if ( developer >= 2 ) Console.Printf("Adding missing achievement progress %s",inf.basename); achievementprogress.Insert(inf.basename,"0"); } } } // save all achievement state and progress private void SaveAchievements() { String statestr = achievementstate.ToString(); String progstr = achievementprogress.ToString(); // trim unneeded json stuff statestr.Replace("{",""); statestr.Replace("}",""); statestr.Replace("\"",""); progstr.Replace("{",""); progstr.Replace("}",""); progstr.Replace("\"",""); // cheap encode static const int cypher[] = {0xAD,0xEA,0xDB,0xED}; String nstr = ""; for ( uint i=0; i>4)+0x41; nstr.AppendFormat("%c%c",b,c); } statestr = nstr; nstr = ""; for ( uint i=0; i>4)+0x41; nstr.AppendFormat("%c%c",b,c); } progstr = nstr; let cv = CVar.FindCVar('swwm_achievementstate'); cv.SetString(statestr); cv = CVar.FindCVar('swwm_achievementprogress'); cv.SetString(progstr); } // blank achievement creation, used if loading fails private void CreateAchievements() { achievementstate = Dictionary.Create(); achievementprogress = Dictionary.Create(); ParseAchievementList(achievementinfo); foreach ( inf:achievementinfo ) { achievementstate.Insert(inf.basename,"0"); if ( !inf.maxval ) continue; achievementprogress.Insert(inf.basename,"0"); } } // migrate from old individual CVars private void MigrateAchievements() { achievementstate = Dictionary.Create(); achievementprogress = Dictionary.Create(); ParseAchievementList(achievementinfo); CVar cv; foreach ( inf:achievementinfo ) { String val = "0"; cv = CVar.FindCVar("swwm_achievement_"..inf.basename); if ( cv ) val = cv.GetString(); achievementstate.Insert(inf.basename,val); if ( !inf.maxval ) continue; val = "0"; cv = CVar.FindCVar("swwm_progress_"..inf.basename); if ( cv ) val = cv.GetString(); achievementprogress.Insert(inf.basename,val); } } }