Talk:Logic achievement
More-or-less full tutorial on adding Steamworks achievements
There's not a great amount of info out there about this and examples are pretty sparse in public codebases; I ended up having to trial-and-error my way through this really. EZ2's code is public and provided me lots of answers and 1upD from their team also lent a hand, so I'm gonna document the process I took so that future creators (and myself!) will benefit from this. Note that I only did the two "simplest" types of achievements - trigger = achievement, and "find x things" = achievement. Stuff like measuring whether a player did all of Ravenholm with just the gravity gun is still outside of my wheelhouse.
First - Steamworks. In the "Stats & Achievements" tab, then the "Achievements" page in that drop down, you need to add your new achievements. The API names should be meaningful so you can tell what they are quickly. If you want to do a "find x things" achievement, you also need to make a Stat; go to the "stats" page, create the relevant stat, set the max change, min value, and max value according to how players will find the things that give you this achievement. Then go back to the "Achievements" page, create your "find x things" achievement, and where it asks for a "Progress Stat", specify the stat you just created for calculating it. Now do the presentation bits - pictures and user-facing text for the achievement, etc.
Second, in code - I created a new achievements_(modname).cpp by copying EZ2's one, renaming it, and wiping it, and added it to the VPC for compiling into the solution. I added this .cpp in sp/src/game/server. Then I copied some relevant code from EZ2's achievements text (one code block for "trigger thing to get achievement", and one code block for "find x things to get achievement"). Here's examples of both from my code:
Trigger event to get achievement class CAchievementGoreHeadsman : public CBaseAchievement { virtual void Init() { static const char* szComponents[] = { "GORE_HEADSMAN" }; SetFlags(ACH_HAS_COMPONENTS | ACH_LISTEN_COMPONENT_EVENTS | ACH_SAVE_GLOBAL); m_pszComponentNames = szComponents; m_iNumComponents = ARRAYSIZE(szComponents); SetComponentPrefix("GORE_HEADSMAN"); SetGameDirFilter("Goreagulation"); SetGoal(1); } };
Find X things to get achievement // Alpha - find all of the meatwoman's narrations class CAchievementGoreFindMeatwoman : public CBaseAchievement { virtual void Init() { static const char* szComponents[] = { "GORE_MEATWOMAN_RG01", "GORE_MEATWOMAN_RG02", "GORE_MEATWOMAN_RG03", "GORE_MEATWOMAN_RG04" }; SetFlags(ACH_HAS_COMPONENTS | ACH_LISTEN_COMPONENT_EVENTS | ACH_SAVE_GLOBAL); m_pszComponentNames = szComponents; m_iNumComponents = ARRAYSIZE(szComponents); SetComponentPrefix("GORE_MEATWOMAN"); SetGameDirFilter("Goreagulation"); SetGoal(m_iNumComponents); }
// Show progress for this achievement virtual bool ShouldShowProgressNotification() { return true; } };
You need to change each of the names specific to the achievement/mod to be specific to your use case.
After you've setup your achievements.cpp, you also need to make changes in sp/src/common/hl2orange.spa.h. You define your achievement IDs here. I added mine after the existing HL2 achievements. Here's how my ID list ended up looking:
//Goreagulation Achievements
- define ACHIEVEMENT_GORE_RESCUER 151
- define ACHIEVEMENT_GORE_DEN 152
- define ACHIEVEMENT_GORE_HEADSMAN 153
- define ACHIEVEMENT_GORE_MEATSPINNERLORE 154
- define ACHIEVEMENT_GORE_GOODENDING 155
- define ACHIEVEMENT_GORE_BADENDING 156
- define ACHIEVEMENT_GORE_MEATWOMAN 157
Ok - that's all the code-side stuff. Thirdly, you need to setup your game's FGD. This is the step that threw me for a loop - I had it all working on Steamworks and in the code (and could see my achievements with debug achievement console commands, which btw only show up if you build the code in debug mode), and couldn't figure out the link into the game. In the end, I again took note of how EZ2 setup their FGD and copied their approach. You need to add your achievement events to the logic_achievement entity. Here's how my FGD for this entity looked for what eventually worked:
@PointClass base(Targetname, EnableDisable) = logic_achievement : "Sends an achievement system related event from the map to the achievement system." [ //keyvalues
AchievementEvent(choices) : "Achievement Event" : 0 : "Named event is sent to the achievement system when this entity receives a 'FireEvent' input." = [ "ACHIEVEMENT_EVENT_GORE_RESCUER" : "[Goreagulation] Receive a reward for your rescue efforts." "ACHIEVEMENT_EVENT_GORE_HEADSMAN" : "[Goreagulation] Decide the fate of an unlucky prisoner." "ACHIEVEMENT_EVENT_GORE_BADENDING" : "[Goreagulation] Grow the Spire. Join the Eye." "ACHIEVEMENT_EVENT_GORE_GOODENDING" : "[Goreagulation] Ascend the Spire. Destroy the Eye." "ACHIEVEMENT_EVENT_GORE_MEATSPINNERLORE" : "[Goreagulation] Discover the truth about the thing It hates most." // "ACHIEVEMENT_EVENT_GORE_DEN_RG01" : "[Goreagulation] Find the writings of those who went before 1." "ACHIEVEMENT_EVENT_GORE_DEN_RG02" : "[Goreagulation] Find the writings of those who went before 2." "ACHIEVEMENT_EVENT_GORE_DEN_RG03" : "[Goreagulation] Find the writings of those who went before 3." "ACHIEVEMENT_EVENT_GORE_DEN_RG04" : "[Goreagulation] Find the writings of those who went before 4." // "ACHIEVEMENT_EVENT_GORE_MEATWOMAN_RG01" : "[Goreagulation] Listen to the First's manifestos 1." "ACHIEVEMENT_EVENT_GORE_MEATWOMAN_RG02" : "[Goreagulation] Listen to the First's manifestos 2." "ACHIEVEMENT_EVENT_GORE_MEATWOMAN_RG03" : "[Goreagulation] Listen to the First's manifestos 3." "ACHIEVEMENT_EVENT_GORE_MEATWOMAN_RG04" : "[Goreagulation] Listen to the First's manifestos 4." ]
// Inputs input Toggle(void) : "Toggle the relay between enabled and disabled." input FireEvent(void) : "Tells the achievement system the specifed event has occured."
// Outputs output OnFired(void) : "When the event fires, this fires." ]
It wasn't immediately obvious to me how to setup the events so that they would tie together; adding the "_EVENT_" syntax appears mandatory, which is where I was slipping up. Notice how for the "Find X Things" achievements, I need one entry per number of things the player has to find.
Okay, lastly - going ingame. Now, put down a logic_achievement entity. For "trigger something once to get achievement", you just need to target the logic_achievement with the "FireEvent" input from a trigger_once, or a logic_relay, or whatever works. For "Find X Things" achievements, this works the same, but you'll need to fire the achievement event for each instance, and if you've set it up correctly you'll see it incrementing on Steam.
It's a bit rough and ready but finding precise instructions anywhere was pretty impossible, and I had to draw together a bunch of stuff to get here. Hopefully this helps anyone (most likely, future me)! --Kralich (talk) 06:28, 11 February 2026 (PST)