24 Feb, 2008, Hades_Kane wrote in the 1st comment:
Votes: 0
There's been a bug that's been around for a bit now that I've been unable to squash.

It happens when I save my newbie zone area, but it doesn't happen 100% of the time. I've been unable to find a common factor for when it happens. But basically, I save the area, then within the next minute or two, the MUD crashes. I've caught the bug in GDB, and here is the output:

Program received signal SIGSEGV, Segmentation fault.
obj_update () at update.c:2086
2086 paf_next = paf->next;
(gdb) bt
#0 obj_update () at update.c:2086
#1 0x08183501 in update_handler () at update.c:2582
#2 0x0809cbc1 in game_loop_unix (control=7) at comm.c:890
#3 0x0809c68d in main (argc=2, argv=0xbf9a1f34) at comm.c:464


and it's said this before as well:

Program terminated with signal 11, Segmentation fault.
#0 0x08182a11 in obj_update () at update.c:2089
2089 paf->duration–;
(gdb) bt
#0 0x08182a11 in obj_update () at update.c:2089
#1 0x08183599 in update_handler () at update.c:2590
#2 0x0809cbc1 in game_loop_unix (control=4) at comm.c:890
#3 0x0809c68d in main (argc=2, argv=0xbf854604) at comm.c:464


As you can see, it's crashing on the paf stuff in the obj_update function in update.c

Here is my obj_update function with the lines it's referencing being line 32 and 35 of the code flag's count:

/*
* Update all objs.
* This function is performance sensitive.
*/
void obj_update( void )
{
OBJ_DATA *obj;
OBJ_DATA *obj_next;
AFFECT_DATA *paf, *paf_next;
AUCTION_DATA *auc;
bool no_update = FALSE;

for ( obj = object_list; obj != NULL; obj = obj_next )
{
CHAR_DATA *rch;
char *message;

obj_next = obj->next;

for (auc = auction_list; auc; auc = auc->next )
{
if (auc->item == NULL || auc->item == obj )
no_update = TRUE;
}
if (no_update)
continue;


/* go through affects and decrement */
for ( paf = obj->affected; paf != NULL; paf = paf_next )
{
paf_next = paf->next;
if ( paf->duration > 0 )
{
paf->duration–;
if (number_range(0,4) == 0 && paf->level > 0)
paf->level–; /* spell strength fades with time */
}
else if ( paf->duration < 0 )
;
else
{
if ( paf_next == NULL
|| paf_next->type != paf->type
|| paf_next->duration > 0 )
{
if ( paf->type > 0 && skill_table[paf->type].msg_obj )
{
if (obj->carried_by != NULL)
{
rch = obj->carried_by;
act(skill_table[paf->type].msg_obj,
rch,obj,NULL,TO_CHAR);
}
if (obj->in_room != NULL
&& obj->in_room->people != NULL)
{
rch = obj->in_room->people;
act(skill_table[paf->type].msg_obj,
rch,obj,NULL,TO_ALL);
}
}
}

affect_remove_obj( obj, paf );
}
}


if ( obj->timer <= 0 || –obj->timer > 0 )
continue;
/*
* Oprog triggers!
*/
if ( obj->in_room || (obj->carried_by && obj->carried_by->in_room))
{
if ( HAS_TRIGGER_OBJ( obj, TRIG_DELAY )
&& obj->oprog_delay > 0 )
{
if ( –obj->oprog_delay <= 0 )
p_percent_trigger( NULL, obj, NULL, NULL, NULL, NULL, TRIG_DELAY );
}
else if ( ((obj->in_room && !obj->in_room->area->empty)
|| obj->carried_by ) && HAS_TRIGGER_OBJ( obj, TRIG_RANDOM ) )
p_percent_trigger( NULL, obj, NULL, NULL, NULL, NULL, TRIG_RANDOM );
}
/* Make sure the object is still there before proceeding */
if ( !obj )
continue;


switch ( obj->item_type )
{
default:
message = "$p crumbles into dust.";
break;
case ITEM_FOUNTAIN:
message = "$p dries up.";
break;
case ITEM_DRINK_CON:
if(obj->pIndexData->vnum == OBJ_VNUM_BLOOD)
message = "$p dries up.";
else
message = "$p crumbles into dust.";
break;
case ITEM_CORPSE_NPC:
message = "$p decays into dust.";
break;
case ITEM_CORPSE_PC:
message = "$p decays into dust.";
break;
case ITEM_FOOD:
message = "$p decomposes.";
break;
case ITEM_POTION:
message = "$p has evaporated from disuse.";
break;
case ITEM_PORTAL:
message = "$p fades out of existence.";
break;
case ITEM_BOMB:
message = " ";
explode(obj);
break;
case ITEM_TRAP:
message = "$p springs without catching anything";
REMOVE_BIT(obj->extra_flags,ITEM_HIDDEN);
break;
case ITEM_CONTAINER:
message = "$p crumbles into dust.";
break;
}

if ( obj->carried_by != NULL )
{
if (IS_NPC(obj->carried_by)
&& obj->carried_by->pIndexData->pShop != NULL)
obj->carried_by->silver += obj->cost/5;
else
{
act( message, obj->carried_by, obj, NULL, TO_CHAR );
}
}
else if ( obj->in_room != NULL
&& ( rch = obj->in_room->people ) != NULL )
{
if (! (obj->in_obj && obj->in_obj->pIndexData->vnum == OBJ_VNUM_PIT
&& !CAN_WEAR(obj->in_obj,ITEM_TAKE)))
{
act( message, rch, obj, NULL, TO_ROOM );
act( message, rch, obj, NULL, TO_CHAR );
}
}

if ((obj->item_type == ITEM_CORPSE_PC)
&& obj->contains)
{ /* save the contents */
OBJ_DATA *t_obj, *next_obj;

for (t_obj = obj->contains; t_obj != NULL; t_obj = next_obj)
{
next_obj = t_obj->next_content;
obj_from_obj(t_obj);

if (obj->in_obj) /* in another object */
obj_to_obj(t_obj,obj->in_obj);

else if (obj->carried_by) /* carried */
obj_to_char(t_obj,obj->carried_by);

else if (obj->in_room == NULL) /* destroy it */
extract_obj(t_obj);

else /* to a room */
obj_to_room(t_obj,obj->in_room);
}
}

extract_obj( obj );
}

return;
}


Like I said, it's been an issue for a while, but normally we don't ever need to edit/save that area file, so it's something I've been putting off. But I need to go through and edit the area to update it on changes made on several key things, and this crash will make it problematic. Although I may have the area updated before a solution is found, I don't like having unknown crash bugs in the game and would like to get this solved more for the sake of having it solved than anything.

Anyone have any ideas? Any help will be greatly appreciated.
24 Feb, 2008, Hades_Kane wrote in the 2nd comment:
Votes: 0
Ok, it deepens. After posting this, I was tinkering a bit, wondering if maybe the problem existed only when I had an item on me or in my inventory from that area. That wasn't the case, as I could have -nothing- on me and it would happen. But in the course of that, I found that any item I load after saving the area immediately crashes the game.

Program received signal SIGSEGV, Segmentation fault.
get_obj_number (obj=0x6f20626f) at handler.c:3588
3588 if (obj->item_type == ITEM_CONTAINER || obj->item_type == ITEM_MONEY
(gdb) bt
#0 get_obj_number (obj=0x6f20626f) at handler.c:3588
#1 0x080d86b1 in get_obj_number (obj=0x6f20626f) at handler.c:3595
#2 0x080d6d80 in obj_to_char (obj=0xb6f04348, ch=0xb6f00894) at handler.c:2423
#3 0x08082071 in do_oload (ch=0xb6f00894, argument=0xb6f00800 "") at act_wiz.c:3291
#4 0x080e2eeb in do_function (ch=0xb6f00800, do_fun=0x8081e5a <do_oload>, argument=0xb6f00800 "") at interp.c:922
#5 0x08081cc6 in do_load (ch=0xb6f00894, argument=0xbf9068e9 "305") at act_wiz.c:3197
#6 0x080e2eba in interpret (ch=0xb6f00894, argument=0xbf9068e5 "obj 305") at interp.c:907
#7 0x0810faed in redit (ch=0xb6f00894, argument=0xb6efc02a "obj 305") at olc.c:684
#8 0x0810f0b6 in run_olc_editor (d=0xb6f00800) at olc.c:50
#9 0x0809cb7d in game_loop_unix (control=7) at comm.c:873
#10 0x0809c68d in main (argc=2, argv=0xbf908694) at comm.c:464


The part of the code being:

/*
* Return # of objects which an object counts as.
* Thanks to Tony Chamberlain for the correct recursive code here.
*/
int get_obj_number( OBJ_DATA *obj )
{
int number;

if (obj->item_type == ITEM_CONTAINER || obj->item_type == ITEM_MONEY
|| obj->item_type == ITEM_GEM || obj->item_type == ITEM_JEWELRY)
number = 0;
else
number = 1;

for ( obj = obj->contains; obj != NULL; obj = obj->next_content )
number += get_obj_number( obj );

return number;
}


It's crashed on an axe and a container being loaded.

So that adds just another layer of the mystery to me, hopefully it will help someone help me figure this out…

I think for now I'll just edit the area file directly and avoid going through the OLC saving of it so I can finish it.

Again, thanks for any help.
24 Feb, 2008, Davion wrote in the 3rd comment:
Votes: 0
Wanna try accessing the 'obj' variable? From frame 0, just type 'p obj->item_type' and see what it says.
24 Feb, 2008, David Haley wrote in the 4th comment:
Votes: 0
Have you tried running this through valgrind? It should be easy if you can reproduce the bug so easily. A lot of that code is checking for nulls, and gdb claims to have a non-null object for the object loading case, so it could be that something weird is going on with your stack at some point.
28 Mar, 2008, Hades_Kane wrote in the 5th comment:
Votes: 0
Forgetful me, I finished editing that area file and since I wasn't crashing the game, kinda forgot I posted this…

Davion: I'm not entirely sure what/how you mean…

David Haley: I managed to crash it three different ways with Valgrind running, actually, and there seems to be a common thread…

The first one was after saving the area then purging an object in the area:

Thu Mar 27 19:53:56 2008 :: Log Diablos: load obj 300
==18131==
==18131== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==18131== Access not within mapped region at address 0x6E612067
==18131== at 0x80D7B6A: extract_obj (handler.c:3000)
==18131== by 0x8082119: do_purge (act_wiz.c:3327)
==18131== by 0x80E2EB9: interpret (interp.c:907)
==18131== by 0x805EB88: substitute_alias (act_info.c:6424)
==18131== by 0x809CB92: game_loop_unix (comm.c:874)
==18131== by 0x809C68C: main (comm.c:464)
==18131==
==18131== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Segmentation fault


The second was when another immortal was trying to login after I saved the area:

Thu Mar 27 19:55:12 2008 :: Loading Ssolvarain.
==21652==
==21652== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==21652== Access not within mapped region at address 0x6E6120AB
==21652== at 0x80D8672: get_obj_number (handler.c:3588)
==21652== by 0x80D86B0: get_obj_number (handler.c:3595)
==21652== by 0x80D6D7F: obj_to_char (handler.c:2423)
==21652== by 0x813AA3D: fread_obj (save.c:2437)
==21652== by 0x8136315: load_char_obj (save.c:824)
==21652== by 0x809F157: nanny (comm.c:2140)
==21652== by 0x809CBA6: game_loop_unix (comm.c:877)
==21652== by 0x809C68C: main (comm.c:464)
==21652==
==21652== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Segmentation fault


And the last was the typical obj_update one:

==25812== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==25812== Access not within mapped region at address 0x5420202E
==25812== at 0x8182989: obj_update (update.c:2086)
==25812== by 0x8183510: update_handler (update.c:2582)
==25812== by 0x809CBC0: game_loop_unix (comm.c:890)
==25812== by 0x809C68C: main (comm.c:464)
==25812==
==25812== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Segmentation fault


Again, my apologies for forgetting about this and not following up on it sooner…
31 Mar, 2008, David Haley wrote in the 6th comment:
Votes: 0
My suspicion from that message is that you are accessing memory that you had previously freed. Does your area save function destroy any objects?
02 Apr, 2008, Hades_Kane wrote in the 7th comment:
Votes: 0
Not that I can tell. Here is my save_area function:

void save_area( AREA_DATA *pArea )
{
FILE *fp;

fclose( fpReserve );
if ( !( fp = fopen( pArea->file_name, "w" ) ) )
{
bug( "Open_area: fopen", 0 );
perror( pArea->file_name );
}

fprintf( fp, "#AREADATA\n" );
fprintf( fp, "Name %s~\n", pArea->name );
fprintf( fp, "Builders %s~\n", fix_string( pArea->builders ) );
fprintf( fp, "VNUMs %ld %ld\n", pArea->min_vnum, pArea->max_vnum );
fprintf( fp, "Credits %s~\n", pArea->credits );
fprintf( fp, "Security %d\n", pArea->security );
fprintf( fp, "Authors %s~\n", fix_string( pArea->authors ) );
fprintf( fp, "Game %s~\n", fix_string( pArea->game ) );
fprintf( fp, "Levels %d %d\n", pArea->low_range, pArea->high_range );
fprintf( fp, "End\n\n\n\n" );

save_mobiles( fp, pArea );
save_objects( fp, pArea );
save_rooms( fp, pArea );
save_specials( fp, pArea );
save_resets( fp, pArea );
save_shops( fp, pArea );
save_mobprogs( fp, pArea );
save_objprogs( fp, pArea );
save_roomprogs( fp, pArea );

if ( pArea->helps && pArea->helps->first )
{
save_helps();
}

fprintf( fp, "#$\n" );
fclose( fp );
fpReserve = fopen( NULL_FILE, "r" );
return;
}


Can you tell anything from that? I imagine I might need to copy some of the functions it calls…
02 Apr, 2008, David Haley wrote in the 8th comment:
Votes: 0
Indeed. I would start with save_objects because this seems to be an issue with objects.
03 Apr, 2008, Hades_Kane wrote in the 9th comment:
Votes: 0
void save_objects( FILE *fp, AREA_DATA *pArea )
{
int i;
OBJ_INDEX_DATA *pObj;

fprintf( fp, "#OBJECTS\n" );

for ( i = pArea->min_vnum; i <= pArea->max_vnum; i++ )
{
if ( (pObj = get_obj_index( i )) )
save_object( fp, pObj );
}

fprintf( fp, "#0\n\n\n\n" );
return;
}


That calls save_object which is:

void save_object( FILE *fp, OBJ_INDEX_DATA *pObjIndex )
{
char letter;
AFFECT_DATA *pAf;
EXTRA_DESCR_DATA *pEd;
char buf[MAX_STRING_LENGTH];
PROG_LIST *pOprog;


fprintf( fp, "#%ld\n", pObjIndex->vnum );
fprintf( fp, "%s~\n", pObjIndex->name );
fprintf( fp, "%s~\n", pObjIndex->short_descr );
fprintf( fp, "%s~\n", fix_string( pObjIndex->description ) );
fprintf( fp, "%s~\n", fix_string( pObjIndex->full_descr ) );
fprintf( fp, "%s ", material_table[pObjIndex->material_type].name);
fprintf( fp, "%s ", item_name(pObjIndex->item_type));
fprintf( fp, "%s ", fwrite_flag( pObjIndex->extra_flags, buf ) );
fprintf( fp, "%s ", fwrite_flag( pObjIndex->race_flags, buf ) );
fprintf( fp, "%s~\n", pObjIndex->oclass );
fprintf( fp, "%s\n", fwrite_flag( pObjIndex->wear_flags, buf ) );

/*
* Using fwrite_flag to write most values gives a strange
* looking area file, consider making a case for each
* item type later.
*/

switch ( pObjIndex->item_type )
{
default:
fprintf( fp, "%s ", fwrite_flag( pObjIndex->value[0], buf ) );
fprintf( fp, "%s ", fwrite_flag( pObjIndex->value[1], buf ) );
fprintf( fp, "%s ", fwrite_flag( pObjIndex->value[2], buf ) );
fprintf( fp, "%s ", fwrite_flag( pObjIndex->value[3], buf ) );
fprintf( fp, "%s\n", fwrite_flag( pObjIndex->value[4], buf ) );
break;

case ITEM_DRINK_CON:
case ITEM_FOUNTAIN:
fprintf( fp, "%ld %ld '%s' %ld %ld\n",
pObjIndex->value[0],
pObjIndex->value[1],
liq_table[pObjIndex->value[2]].liq_name,
pObjIndex->value[3],
pObjIndex->value[4]);
break;

case ITEM_AIRSHIP:
fprintf (fp, "%s %ld %ld %ld %ld\n",
fwrite_flag (pObjIndex->value[0], buf),
pObjIndex->value[1], pObjIndex->value[2],
pObjIndex->value[3], pObjIndex->value[4]);
break;

case ITEM_CONTAINER:
fprintf( fp, "%ld %s %ld %ld %ld\n",
pObjIndex->value[0],
fwrite_flag( pObjIndex->value[1], buf ),
pObjIndex->value[2],
pObjIndex->value[3],
pObjIndex->value[4]);
break;

case ITEM_WEAPON:
fprintf( fp, "%s %ld %ld %s %s\n",
weapon_name(pObjIndex->value[0]),
pObjIndex->value[1],
pObjIndex->value[2],
attack_table[pObjIndex->value[3]].name,
fwrite_flag( pObjIndex->value[4], buf ) );
break;

case ITEM_ARROW:
fprintf( fp, "%ld %ld %ld %s %s\n",
pObjIndex->value[0],
pObjIndex->value[1],
pObjIndex->value[2],
arrow_table[pObjIndex->value[3]].name,
fwrite_flag( pObjIndex->value[4], buf ) );
break;

case ITEM_PILL:
case ITEM_POTION:
case ITEM_SCROLL:
fprintf( fp, "%ld '%s' '%s' '%s' '%s'\n",
pObjIndex->value[0] > 0 ? /* no negative numbers */
pObjIndex->value[0]
: 0,
pObjIndex->value[1] != -1 ?
skill_table[pObjIndex->value[1]].name
: "",
pObjIndex->value[2] != -1 ?
skill_table[pObjIndex->value[2]].name
: "",
pObjIndex->value[3] != -1 ?
skill_table[pObjIndex->value[3]].name
: "",
pObjIndex->value[4] != -1 ?
skill_table[pObjIndex->value[4]].name
: "");
break;

case ITEM_STAFF:
case ITEM_WAND:
fprintf( fp, "%ld %ld %ld '%s' %ld\n",
pObjIndex->value[0],
pObjIndex->value[1],
pObjIndex->value[2],
pObjIndex->value[3] != -1 ?
skill_table[pObjIndex->value[3]].name :
"",
pObjIndex->value[4] );
break;

}

fprintf( fp, "%d ", pObjIndex->level );
fprintf( fp, "%d ", pObjIndex->weight );
fprintf( fp, "%d ", pObjIndex->cost );
fprintf(fp, "%ld ", pObjIndex->link );

if ( pObjIndex->condition > 90 ) letter = 'P';
else if ( pObjIndex->condition > 75 ) letter = 'G';
else if ( pObjIndex->condition > 50 ) letter = 'A';
else if ( pObjIndex->condition > 25 ) letter = 'W';
else if ( pObjIndex->condition > 10 ) letter = 'D';
else if ( pObjIndex->condition > 0 ) letter = 'B';
else letter = 'R';

fprintf( fp, "%c\n", letter );

for ( pAf = pObjIndex->affected; pAf; pAf = pAf->next )
{
if (pAf->where == TO_OBJECT ||
(pAf->bitvector == 0 && pAf->where != TO_SKILLS))
fprintf( fp, "A\n%d %d\n", pAf->location, pAf->modifier );
else
{
fprintf( fp, "F\n" );

switch (pAf->where)
{
case TO_AFFECTS:
fprintf( fp, "A " );
break;
case TO_IMMUNE:
fprintf( fp, "I " );
break;
case TO_ABSORB:
fprintf( fp, "B " );
break;
case TO_RESIST:
fprintf( fp, "R " );
break;
case TO_VULN:
fprintf( fp, "V " );
break;
case TO_SKILLS:
fprintf( fp, "S " );
break;
default:
bug( "olc_save: Invalid Affect->where", 0);
break;
}
if (pAf->where == TO_SKILLS)
fprintf( fp, "'%s' %d\n",
skill_table[pAf->type].name, pAf->modifier);
else
fprintf( fp, "%d %d %s\n",
pAf->location, pAf->modifier,
fwrite_flag( pAf->bitvector, buf ) );
}
}

for ( pEd = pObjIndex->extra_descr; pEd; pEd = pEd->next )
{
fprintf( fp, "E\n%s~\n%s~\n", pEd->keyword,
fix_string( pEd->description ) );
}
for (pOprog = pObjIndex->oprogs; pOprog; pOprog = pOprog->next)
{
fprintf(fp, "O %s %ld %s~\n",
prog_type_to_name(pOprog->trig_type), pOprog->vnum,
pOprog->trig_phrase);
}

return;
}


Again, I appreciate the help.
03 Apr, 2008, David Haley wrote in the 10th comment:
Votes: 0
I guess that looks ok to me; I don't see anything that deletes effects.

What I would do at this point is make the area parameter to save_area const, and adapt all of the functions to use const. Then if something is trying to modify the memory – and nothing should be modifying memory in a save function! (see note below) – the compiler will inform you of that. This is the kind of situation that proper use of const in the first place helps avoid.

I can think of one legitimate modification of an object you are saving, and that is the last-saved time. One solution to that is to not make save_area take a const, but have all helper functions take const.

The prototype would change from, e.g.:
void save_objects( FILE *fp, AREA_DATA *pArea )

to
void save_objects( FILE *fp, const AREA_DATA *pArea )


Of course, this will take a while because you'll have to propagate the const-ness to all helper functions, but in the end it is worth it, and if something is being modified where it shouldn't, you will probably find it.

The other option is to debug this the "proper way" and just step through the code (using a debugger) as you save a relatively small area. You could do it by hand reading the code, but I imagine you've already read all code relating to area saving. Using the debugger will show you *exactly* what is run, and you'll be able to see from that what exactly happened.

Basically, you need to establish the full trace of events starting from the point where the MUD interprets the command to save the area, and figure out if *anything* was modified. gdb probably has memory watches but I don't know off-hand how to use them: I've never had to.
18 Jul, 2008, Hades_Kane wrote in the 11th comment:
Votes: 0
We finally fixed these issues, so I thought I'd update everyone on what it was…

One of my staff knows a lot more about GDB, linux, and in general C than I do, though his experience with MUD code is a bit limited. So with his help, we isolated many of the things that were crashing and fixed those. Turns out the area saving bug was exposing some other sloppy ROM coding, such as objects being created without the affects part of it being initialized, and so on an object being created, and before any affects are applied, we have it initialized to NULL and the paf->update thing quit crashing. Well, we fixed as many of the instances of the sloppy coding, but the root of the problem, something going wrong when the areas were being saved, was still there. So I dug around some more…

Basically, it was the fix_string function seen above. Saving objects were never the problem, rather, the different areas that were crashing were doing so on different save functions. Our newbie zone was messing things up with save_mobprogs ran, while another area I was testing with was messing up after save_rooms.

I knew that our newbie zone had really, really big mobprogs, and this other area had really, really big descriptions (as a result of the descriptions being ASCII art). So, that got me thinking and I crawled through the code again looking for any sort of input, buffer, or character limit that might be being surpassed.

What I found was in fix_string…

char *fix_string( const char *str )
{
static char strfix[MAX_STRING_LENGTH * 2];
int i;
int o;

if ( str == NULL )
return '\0';

for ( o = i = 0; str[i+o] != '\0'; i++ )
{
if (str[i+o] == '\r' || str[i+o] == '~')
o++;
strfix[i] = str[i+o];
}
strfix[i] = '\0';
return strfix;
}


I honed in on:
static char strfix[MAX_STRING_LENGTH * 2];


I increased the '* 2' to increase the overall size of strfix, and immediately several of the areas that were having issue no longer were. I had to gradually increase that number until the most notable of the problem areas, our newbie zone, was no longer crashing. Now I can save any of the areas, and even asave world, with no problems, and it's been a while since we've been able to do that.

So as far as I can tell, everything is fine now.

I appreciate the help I was offered, and I'm glad to have finally gotten to the bottom of this.
20 Aug, 2008, Sandi wrote in the 12th comment:
Votes: 0
A month late, but thanks for the update. We just installed OLC, so things like this are good to know.
0.0/12