/* Copyright (C) 1996-1997 Id Software, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // cvar.c -- dynamic variable tracking #include "quakedef.h" cvar_group_t *cvar_groups; //cvar_t *cvar_vars; char *cvar_null_string = ""; cvar_const_cache_t cc_cache; // cvar string cache functions // CCC_AddString: adds a string into the cvar CB, and creates one // if non-existant char *CCC_AddString(char *s) { int size = strlen(s) + 1; if (!cc_cache.cb) cc_cache.cb = CB_Malloc(CC_CACHE_SIZE, CC_CACHE_STEP); return CB_Copy (cc_cache.cb, s, size); } // CCC_CheckAndAddString: checks against the CCC table, updates table, // and either adds or returns cached string char *CCC_CheckAndAddString(char *s) { int i; char *c; for (i = 0; i < CC_CACHE_ENTRIES; i++) { if (cc_cache.cached[i] && !strcmp(s, cc_cache.cached[i])) { // move string to top, pushing others down c = cc_cache.cached[i]; if (i != 0) { Q_memcpy(cc_cache.cached + 1, cc_cache.cached, sizeof(char*) * i); cc_cache.cached[0] = c; } return c; } } // not in cache, so add it to table // move current cached strings down Q_memcpy(cc_cache.cached + 1, cc_cache.cached, sizeof(char*) * (CC_CACHE_ENTRIES - 1)); return (cc_cache.cached[0] = CCC_AddString(s)); } /* ============ Cvar_FindVar ============ */ cvar_t *Cvar_FindVar (const char *var_name) { cvar_group_t *grp; cvar_t *var; for (grp=cvar_groups ; grp ; grp=grp->next) for (var=grp->cvars ; var ; var=var->next) if (!Q_strcasecmp (var_name, var->name)) return var; for (grp=cvar_groups ; grp ; grp=grp->next) for (var=grp->cvars ; var ; var=var->next) if (var->name2 && !Q_strcasecmp (var_name, var->name2)) return var; return NULL; } cvar_group_t *Cvar_FindGroup (const char *group_name) { cvar_group_t *grp; for (grp=cvar_groups ; grp ; grp=grp->next) if (!Q_strcasecmp (group_name, grp->name)) return grp; return NULL; } cvar_group_t *Cvar_GetGroup(const char *gname) { cvar_group_t *g; if (!gname) gname = "Miscilaneous vars"; g = Cvar_FindGroup(gname); if (g) return g; g = (cvar_group_t*)Z_Malloc(sizeof(cvar_group_t) + strlen(gname)+1); g->name = (char*)(g+1); strcpy((char*)g->name, gname); g->next = NULL; g->next = cvar_groups; cvar_groups = g; return g; } // converts a given single cvar flag into a human readable string char *Cvar_FlagToName(int flag) { switch (flag) { case CVAR_ARCHIVE: return "archive"; case CVAR_USERINFO: return "userinfo"; case CVAR_SERVERINFO: return "serverinfo"; case CVAR_NOSET: return "noset"; case CVAR_LATCH: return "latch"; case CVAR_POINTER: return "pointer"; case CVAR_NOTFROMSERVER: return "noserver"; case CVAR_USERCREATED: return "userset"; case CVAR_CHEAT: return "cheat"; case CVAR_SEMICHEAT: return "semicheat"; case CVAR_RENDERERLATCH: return "renderlatch"; case CVAR_SERVEROVERRIDE: return "serverlatch"; } return NULL; } //lists commands, also prints restriction level #define CLF_RAW 0x1 #define CLF_LEVEL 0x2 #define CLF_ALTNAME 0x4 #define CLF_VALUES 0x8 #define CLF_DEFAULT 0x10 #define CLF_LATCHES 0x20 #define CLF_FLAGS 0x40 #define CLF_FLAGMASK 0x80 void Cvar_List_f (void) { cvar_group_t *grp; cvar_t *cmd; char *var, *search, *gsearch; int gnum, i, num = 0; int listflags = 0, cvarflags = 0; char strtmp[512]; gsearch = search = NULL; for (i = 1; i < Cmd_Argc(); i++) { var = Cmd_Argv(i); if (*var == '-') { // short options for (var++; *var; var++) { switch (*var) { case 'g': // fix this so we can search for multiple groups i++; if (i >= Cmd_Argc()) { Con_Printf("Missing parameter for -g\nUse cvarlist -h for help\n"); return; } gsearch = Cmd_Argv(i); break; case 'a': listflags |= CLF_ALTNAME; break; case 'l': listflags |= CLF_LEVEL; break; case 'r': listflags |= CLF_RAW; break; case 'v': listflags |= CLF_VALUES; break; case 'd': listflags |= CLF_DEFAULT; break; case 'L': listflags |= CLF_LATCHES; break; case 'f': { char *tmpv; listflags |= CLF_FLAGMASK; i++; if (i >= Cmd_Argc()) { Con_Printf("Missing parameter for -f\nUse cvarlist -h for help\n"); return; } tmpv = Cmd_Argv(i); for (num = 1; num <= CVAR_LASTFLAG; num <<= 1) { char *tmp; tmp = Cvar_FlagToName(num); if (tmp && !stricmp(tmp, tmpv)) { cvarflags |= num; break; } } if (num > CVAR_LASTFLAG) { Con_Printf("Invalid cvar flag name\nUse cvarlist -h for help\n"); return; } } break; case 'F': listflags |= CLF_FLAGS; break; case 'h': // list options Con_Printf("cvarlist list all cvars matching given parameters\n" "Syntax: cvarlist [-FLdhlrv] [-f flag] [-g group] [cvar]\n" " -F shows cvar flags\n" " -L shows latched values\n" " -a shows cvar alternate names\n" " -d shows default cvar values\n" " -f shows only cvars with a matching flag, more than one -f can be used\n" " -g shows only cvar groups using wildcards in group\n" " -h shows this help message\n" " -l shows cvar restriction levels\n" " -r removes group and list headers\n" " -v shows current values\n" " cvar indicates the cvar to show, wildcards (*,?) accepted\n" "Cvar flags are:"); for (num = 1; num <= CVAR_LASTFLAG; num <<= 1) { // no point caring about the content of var at this point var = Cvar_FlagToName(num); if (var) Con_Printf(" %s", var); } Con_Printf("\n\n"); return; case '-': break; default: Con_Printf("Invalid option for cvarlist\nUse cvarlist -h for help\n"); return; } } } else search = var; } // this is sane.. hopefully if (gsearch) Q_strlwr(gsearch); if (search) Q_strlwr(search); for (grp=cvar_groups ; grp ; grp=grp->next) { // list only cvars with group search substring if (gsearch) { Q_strncpyz(strtmp, grp->name, 512); Q_strlwr(strtmp); if (!wildcmp(gsearch, strtmp)) continue; } gnum = 0; for (cmd=grp->cvars ; cmd ; cmd=cmd->next) { // list only non-restricted cvars if ((cmd->restriction?cmd->restriction:rcon_level.value) > Cmd_ExecLevel) continue; // list only cvars with search substring if (search) { Q_strncpyz(strtmp, cmd->name, 512); Q_strlwr(strtmp); if (!wildcmp(search, strtmp)) { if (cmd->name2) { Q_strncpyz(strtmp, cmd->name2, 512); Q_strlwr(strtmp); if (!wildcmp(search, strtmp)) continue; } else continue; } } // list only cvars with matching flags if ((listflags & CLF_FLAGMASK) && !(cmd->flags & cvarflags)) continue; // print cvar list header if (!(listflags & CLF_RAW) && !num) Con_TPrintf(TL_CVARLISTHEADER); // print group header if (!(listflags & CLF_RAW) && !gnum) Con_Printf("%s --\n", grp->name); // print restriction level if (listflags & CLF_LEVEL) Con_Printf("(%i) ", cmd->restriction); // print cvar name Con_Printf(cmd->name); // print current value if (listflags & CLF_VALUES) { if (*cmd->string) Con_Printf(" %s", cmd->string); } // print default value if (listflags & CLF_DEFAULT) Con_Printf(", default \"%s\"", cmd->defaultstr); // print alternate name if ((listflags & CLF_ALTNAME) && cmd->name2) Con_Printf(", alternate %s", cmd->name2); // print cvar flags if (listflags & CLF_FLAGS) { for (i = 1; i <= CVAR_LASTFLAG; i <<= 1) { if (i & cmd->flags) { var = Cvar_FlagToName(i); if (var) Con_Printf(" %s", var); } } } // print latched value if (listflags & CLF_LATCHES) { if (cmd->latched_string) Con_Printf(", latched as \"%s\"", cmd->latched_string); } // print new line to finish individual cvar Con_Printf("\n"); num++; gnum++; } // print new line to seperate groups if (!(listflags & CLF_RAW) && gnum) Con_Printf("\n"); } } #define CRF_ALTNAME 0x1 void Cvar_Reset_f (void) { cvar_group_t *grp; cvar_t *cmd; int i, listflags=0, exclflags; char *var; char *search, *gsearch; char strtmp[512]; search = gsearch = NULL; exclflags = 0; // parse command line options for (i = 1; i < Cmd_Argc(); i++) { var = Cmd_Argv(i); if (*var == '-') { // short options for (var++; *var; var++) { switch (*var) { case 'a': listflags |= CRF_ALTNAME; break; case 'g': // fix this so we can search for multiple groups i++; if (i >= Cmd_Argc()) { Con_Printf("Missing parameter for -g\nUse cvarlist -h for help\n"); return; } gsearch = Cmd_Argv(i); break; case 'u': exclflags |= CVAR_USERCREATED; case 'h': Con_Printf("cvarreset resets all cvars to default values matching given parameters\n" "Syntax: cvarreset [-ahu] (-g group)/cvar\n" " -a matches cvar against alternate cvar names\n" " -g matches using wildcards in group\n" " -h shows this help message\n" " -u excludes user cvars\n" " cvar indicates the cvars to reset, wildcards (*, ?) accepted\n" "A -g or cvar is required\n"); return; default: Con_Printf("Invalid option for cvarreset\nUse cvarreset -h for help\n"); return; } } } else search = var; } if (!search && !gsearch) { Con_Printf("No group or cvars given\nUse cvarreset -h for help\n"); return; } // this should be sane.. hopefully if (search) Q_strlwr(search); if (gsearch) Q_strlwr(gsearch); for (grp=cvar_groups ; grp ; grp=grp->next) { if (gsearch) { Q_strncpyz(strtmp, grp->name, 512); Q_strlwr(strtmp); if (!wildcmp(gsearch, strtmp)) continue; } for (cmd=grp->cvars ; cmd ; cmd=cmd->next) { // reset only non-restricted cvars if ((cmd->restriction?cmd->restriction:rcon_level.value) > Cmd_ExecLevel) continue; // don't reset cvars with matched flags if (exclflags & cmd->flags) continue; // reset only cvars with search substring if (search) { Q_strncpyz(strtmp, cmd->name, 512); Q_strlwr(strtmp); if (!wildcmp(search, strtmp)) { if ((listflags & CRF_ALTNAME) && cmd->name2) { Q_strncpyz(strtmp, cmd->name2, 512); Q_strlwr(strtmp); if (!wildcmp(search, strtmp)) continue; } else continue; } } // reset cvar to default Cvar_Set(cmd, cmd->defaultstr); } } } /* ============ Cvar_VariableValue ============ */ float Cvar_VariableValue (const char *var_name) { cvar_t *var; var = Cvar_FindVar (var_name); if (!var) return 0; return Q_atof (var->string); } /* ============ Cvar_VariableString ============ */ char *Cvar_VariableString (const char *var_name) { cvar_t *var; var = Cvar_FindVar (var_name); if (!var) return cvar_null_string; return var->string; } /* ============ Cvar_CompleteVariable ============ */ /* moved to cmd_compleatevariable char *Cvar_CompleteVariable (char *partial) { cvar_group_t *grp; cvar_t *cvar; int len; len = Q_strlen(partial); if (!len) return NULL; // check exact match for (grp=cvar_groups ; grp ; grp=grp->next) for (cvar=grp->cvars ; cvar ; cvar=cvar->next) if (!strcmp (partial,cvar->name)) return cvar->name; // check partial match for (grp=cvar_groups ; grp ; grp=grp->next) for (cvar=grp->cvars ; cvar ; cvar=cvar->next) if (!Q_strncmp (partial,cvar->name, len)) return cvar->name; return NULL; } */ /* ============ Cvar_Set ============ */ cvar_t *Cvar_SetCore (cvar_t *var, const char *value, qboolean force) { char *latch=NULL; if (!var) return NULL; if ((var->flags & CVAR_NOSET) && !force) { Con_Printf ("variable %s is readonly\n", var->name); return NULL; } if (var->flags & CVAR_SERVEROVERRIDE && !force) latch = "variable %s is under server control - latched\n"; else if (var->flags & CVAR_LATCH) latch = "variable %s is latched\n"; else if (var->flags & CVAR_RENDERERLATCH && qrenderer) latch = "variable %s will be changed after a renderer restart\n"; #ifndef SERVERONLY else if (var->flags & CVAR_CHEAT && !cls.allow_cheats && cls.state) latch = "variable %s is a cheat variable - latched\n"; else if (var->flags & CVAR_SEMICHEAT && !cls.allow_semicheats && cls.state) latch = "variable %s is a cheat variable - latched\n"; #endif if (latch && !force) { if (cl_warncmd.value) { if (var->latched_string) { //already latched if (strcmp(var->latched_string, value)) Con_Printf (latch, var->name); } else { //new latch if (strcmp(var->string, value)) Con_Printf (latch, var->name); } } if (var->latched_string && !strcmp(var->latched_string, value)) //no point, this would force the same return NULL; if (var->latched_string) Z_Free(var->latched_string); if (!strcmp(var->string, value)) //latch to the original value? remove the latch. { var->latched_string = NULL; return NULL; } var->latched_string = (char*)Z_Malloc(strlen(value)+1); strcpy(var->latched_string, value); return NULL; } #ifndef CLIENTONLY if (var->flags & CVAR_SERVERINFO) { Info_SetValueForKey (svs.info, var->name, value, MAX_SERVERINFO_STRING); SV_SendServerInfoChange(var->name, value); // SV_BroadcastCommand ("fullserverinfo \"%s\"\n", svs.info); } #endif #ifndef SERVERONLY if (var->flags & CVAR_USERINFO) { Info_SetValueForKey (cls.userinfo, var->name, value, MAX_INFO_STRING); if (cls.state >= ca_connected) { #ifdef Q2CLIENT if (cls.protocol == CP_QUAKE2 || cls.protocol == CP_QUAKE3) //q2 just resends the lot. Kinda bad... { cls.resendinfo = true; } else #endif { CL_SendClientCommand(true, "setinfo \"%s\" \"%s\"\n", var->name, value); } } } #endif latch = var->string; var->string = (char*)Z_Malloc (Q_strlen(value)+1); Q_strcpy (var->string, value); var->value = Q_atof (var->string); if (latch) { if (strcmp(latch, value)) var->modified++; //only modified if it changed. Z_Free (latch); // free the old value string } if (var->latched_string) //we may as well have this here. { Z_Free(var->latched_string); var->latched_string = NULL; } return var; } void Cvar_ForceCheatVars(qboolean semicheats, qboolean absolutecheats) { //this either unlatches if the cheat type is allowed, or enforces a default for full cheats and blank for semicheats. //this is clientside only. //if a value is enforced, it is latched to the old value. cvar_group_t *grp; cvar_t *var; char *latch; for (grp=cvar_groups ; grp ; grp=grp->next) for (var=grp->cvars ; var ; var=var->next) { if (!(var->flags & (CVAR_CHEAT|CVAR_SEMICHEAT))) continue; latch = var->latched_string; var->latched_string = NULL; if (!latch) { latch = var->string; var->string = NULL; } if (var->flags & CVAR_CHEAT) { if (!absolutecheats) Cvar_ForceSet(var, var->defaultstr); else Cvar_ForceSet(var, latch); } if (var->flags & CVAR_SEMICHEAT) { if (!semicheats) Cvar_ForceSet(var, ""); else Cvar_ForceSet(var, latch); } if (latch) { if (!strcmp(var->string, latch)) Z_Free(latch); else var->latched_string = latch; } } } void Cvar_ApplyLatches(int latchflag) { cvar_group_t *grp; cvar_t *var; int mask = ~0; if (latchflag == CVAR_SERVEROVERRIDE) //these ones are cleared mask = ~CVAR_SERVEROVERRIDE; for (grp=cvar_groups ; grp ; grp=grp->next) for (var=grp->cvars ; var ; var=var->next) { if (var->flags & latchflag) { if (var->latched_string) { Cvar_ForceSet(var, var->latched_string); } var->flags &= mask; } } } cvar_t *Cvar_Set (cvar_t *var, const char *value) { return Cvar_SetCore(var, value, false); } cvar_t *Cvar_ForceSet (cvar_t *var, const char *value) { return Cvar_SetCore(var, value, true); } /* ============ Cvar_SetValue ============ */ void Cvar_SetValue (cvar_t *var, float value) { char val[32]; if (value == (int)value) sprintf (val, "%i",(int)value); //make it look nicer. else sprintf (val, "%f",value); Cvar_Set (var, val); } void Cvar_Free(cvar_t *tbf) { cvar_t *var; cvar_group_t *grp; if (!(tbf->flags & CVAR_POINTER)) return; //only freeable if it was a pointer to begin with. for (grp=cvar_groups ; grp ; grp=grp->next) { if (grp->cvars == tbf) { grp->cvars = tbf->next; goto unlinked; } for (var=grp->cvars ; var->next ; var=var->next) { if (var->next == tbf) { var->next = tbf->next; goto unlinked; } } } unlinked: Z_Free(tbf->string); // Z_Free(tbf->defaultstr); if (tbf->latched_string) Z_Free(tbf->latched_string); Z_Free(tbf); } /* ============ Cvar_RegisterVariable Adds a freestanding variable to the variable list. ============ */ qboolean Cvar_Register (cvar_t *variable, const char *groupname) { cvar_t *old; cvar_group_t *group; char value[512]; // copy the value off, because future sets will Z_Free it strcpy (value, variable->string); // check to see if it has already been defined old = Cvar_FindVar (variable->name); if (old) { if (old->flags & CVAR_POINTER) { group = Cvar_GetGroup(groupname); variable->modified = old->modified; variable->flags |= old->flags & CVAR_ARCHIVE; // link the variable in variable->next = group->cvars; variable->restriction = old->restriction; //exe registered vars group->cvars = variable; // make sure it can be zfreed variable->string = (char*)Z_Malloc (1); //cheat prevention - engine set default is the one that stays. variable->defaultstr = CCC_CheckAndAddString(value); //give it it's default (for server controlled vars and things) // set it through the function to be consistant if (old->latched_string) Cvar_SetCore (variable, old->latched_string, true); else Cvar_SetCore (variable, old->string, true); Cvar_Free(old); return false; } Con_Printf ("Can't register variable %s, already defined\n", variable->name); return false; } // check for overlap with a command if (Cmd_Exists (variable->name)) { Con_Printf ("Cvar_RegisterVariable: %s is a command\n", variable->name); return false; } group = Cvar_GetGroup(groupname); // link the variable in variable->next = group->cvars; variable->restriction = 0; //exe registered vars group->cvars = variable; variable->string = (char*)Z_Malloc (1); variable->defaultstr = CCC_CheckAndAddString(value); //give it it's default (for server controlled vars and things) // set it through the function to be consistant Cvar_SetCore (variable, value, true); return true; } /* void Cvar_RegisterVariable (cvar_t *variable) { Cvar_Register(variable, NULL); } */ cvar_t *Cvar_Get(const char *name, const char *defaultvalue, int flags, const char *group) { cvar_t *var; var = Cvar_FindVar(name); if (var) { //allow this to change all < cvar_latch values. //this allows q2 dlls to apply different flags to a cvar without destroying our important ones (like cheat). var->flags = (var->flags & ~(CVAR_NOSET)) | (flags & (CVAR_NOSET|CVAR_SERVERINFO|CVAR_USERINFO|CVAR_ARCHIVE)); return var; } var = (cvar_t*)Z_Malloc(sizeof(cvar_t)+strlen(name)+1); var->name = (char *)(var+1); strcpy(var->name, name); var->string = (char*)defaultvalue; var->flags = flags|CVAR_POINTER|CVAR_USERCREATED; if (!Cvar_Register(var, group)) return NULL; return var; } //prevent the client from altering the cvar until they change map or the server resets the var to the default. void Cvar_LockFromServer(cvar_t *var, const char *str) { char *oldlatch; Con_DPrintf("Server taking control of cvar %s (%s)\n", var->name, str); var->flags |= CVAR_SERVEROVERRIDE; oldlatch = var->latched_string; if (oldlatch) //maintaining control var->latched_string = NULL; else //taking control { oldlatch = (char*)Z_Malloc(strlen(var->string)+1); strcpy(oldlatch, var->string); } Cvar_SetCore (var, str, true); //will use all, quote included var->latched_string = oldlatch; //keep track of the original value. } /* ============ Cvar_Command Handles variable inspection and changing from the console ============ */ qboolean Cvar_Command (int level) { cvar_t *v; char *str; // check variables v = Cvar_FindVar (Cmd_Argv(0)); if (!v) return false; if ((v->restriction?v->restriction:rcon_level.value) > level) { Con_Printf ("You do not have the priveledges for %s\n", v->name); return true; } if (v->flags & CVAR_NOTFROMSERVER && Cmd_FromGamecode()) { Con_Printf ("Server tried setting %s cvar\n", v->name); return true; } // perform a variable print or set if (Cmd_Argc() == 1) { Con_Printf ("\"%s\" is \"%s\"\n", v->name, v->string); if (v->latched_string) Con_Printf ("Latched as \"%s\"\n", v->latched_string); Con_Printf("Default: \"%s\"\n", v->defaultstr); return true; } if (Cmd_Argc() == 2) str = Cmd_Argv(1); else str = Cmd_Args(); if (v->flags & CVAR_NOSET) { Con_Printf ("Cvar %s may not be set via the console\n", v->name); return true; } #ifndef SERVERONLY if (Cmd_ExecLevel > RESTRICT_SERVER) { //directed at a secondary player. CL_SendClientCommand(true, "%i setinfo %s \"%s\"", Cmd_ExecLevel - RESTRICT_SERVER-1, v->name, str); return true; } if (v->flags & CVAR_SERVEROVERRIDE) { if (Cmd_FromGamecode()) { if (!strcmp(v->defaultstr, str)) //returning to default { v->flags &= ~CVAR_SERVEROVERRIDE; if (v->latched_string) str = v->latched_string; //set to the latched } else { Cvar_LockFromServer(v, str); return true; } } //let cvar_set latch if needed. } else if (Cmd_FromGamecode()) {//it's not latched yet if (strcmp(v->defaultstr, str)) { //lock the cvar, unless it's going to it's default value. Cvar_LockFromServer(v, str); return true; } } #endif Cvar_Set (v, str); //will use all, quote included return true; } /* ============ Cvar_WriteVariables Writes lines containing "set variable value" for all variables with the archive flag set to true. ============ */ void Cvar_WriteVariables (vfsfile_t *f, qboolean all) { qboolean writtengroupheader; cvar_group_t *grp; cvar_t *var; char *val; char *s; for (grp=cvar_groups ; grp ; grp=grp->next) { writtengroupheader = false; for (var = grp->cvars ; var ; var = var->next) if (var->flags & CVAR_ARCHIVE || all) { if (!writtengroupheader) { writtengroupheader = true; s = va("\n// %s\n", grp->name); VFS_WRITE(f, s, strlen(s)); } val = var->string; //latched vars should act differently. if (var->latched_string) val = var->latched_string; if (var->flags & CVAR_USERCREATED) { if (var->flags & CVAR_ARCHIVE) s = va("seta %s \"%s\"\n", var->name, val); else s = va("set %s \"%s\"\n", var->name, val); } else s = va("%s \"%s\"\n", var->name, val); VFS_WRITE(f, s, strlen(s)); } } } void Cvar_Shutdown(void) { cvar_t *var; cvar_group_t *grp; while(cvar_groups) { while(cvar_groups->cvars) { var = cvar_groups->cvars; cvar_groups->cvars = var->next; Z_Free(var->string); if (var->flags & CVAR_POINTER) Z_Free(var); } grp = cvar_groups; cvar_groups = grp->next; Z_Free(grp); } if (cc_cache.cb) CB_Free(cc_cache.cb); }