Atrinik Server  4.0
account.c
Go to the documentation of this file.
1 /*************************************************************************
2  * Atrinik, a Multiplayer Online Role Playing Game *
3  * *
4  * Copyright (C) 2009-2014 Alex Tokar and Atrinik Development Team *
5  * *
6  * Fork from Crossfire (Multiplayer game for X-windows). *
7  * *
8  * This program is free software; you can redistribute it and/or modify *
9  * it under the terms of the GNU General Public License as published by *
10  * the Free Software Foundation; either version 2 of the License, or *
11  * (at your option) any later version. *
12  * *
13  * This program is distributed in the hope that it will be useful, *
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16  * GNU General Public License for more details. *
17  * *
18  * You should have received a copy of the GNU General Public License *
19  * along with this program; if not, write to the Free Software *
20  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. *
21  * *
22  * The author can be reached at admin@atrinik.org *
23  ************************************************************************/
24 
32 #include <global.h>
33 #include <toolkit/packet.h>
34 #include <toolkit/string.h>
35 #include <arch.h>
36 #include <player.h>
37 #include <toolkit/path.h>
38 #include <toolkit/datetime.h>
39 #include <toolkit/pbkdf2.h>
40 
41 #define ACCOUNT_CHARACTERS_LIMIT 16
42 #define ACCOUNT_PASSWORD_SIZE 32
43 #define ACCOUNT_PASSWORD_ITERATIONS 4096
44 
45 typedef struct account_struct {
46  unsigned char password[ACCOUNT_PASSWORD_SIZE];
47 
48  unsigned char salt[ACCOUNT_PASSWORD_SIZE];
49 
50  char *password_old;
51 
52  char *last_host;
53 
54  time_t last_time;
55 
56  struct {
57  archetype_t *at;
58 
59  char *name;
60 
61  char *region_name;
62 
63  uint8_t level;
64  } *characters;
65 
66  size_t characters_num;
68 
69 void account_init(void)
70 {
71 }
72 
73 void account_deinit(void)
74 {
75 }
76 
77 static void account_free(account_struct *account)
78 {
79  size_t i;
80 
81  if (account->last_host) {
82  efree(account->last_host);
83  }
84 
85  if (account->password_old) {
86  efree(account->password_old);
87  }
88 
89  for (i = 0; i < account->characters_num; i++) {
90  efree(account->characters[i].name);
91  efree(account->characters[i].region_name);
92  }
93 
94  if (account->characters) {
95  efree(account->characters);
96  }
97 }
98 
99 static char *account_old_crypt(char *str, const char *salt)
100 {
101 #if defined(HAVE_CRYPT) && defined(HAVE_CRYPT_H)
102  return crypt(str, salt);
103 #else
104  return str;
105 #endif
106 }
107 
108 static void account_set_password(account_struct *account, const char *password)
109 {
110  size_t i;
111 
112  /* Create a truly random 256-bit salt. */
113  for (i = 0; i < ACCOUNT_PASSWORD_SIZE; i++) {
114  account->salt[i] = rndm(1, 256) - 1;
115  }
116 
117  PKCS5_PBKDF2_HMAC_SHA2((const unsigned char *) password, strlen(password),
118  account->salt, ACCOUNT_PASSWORD_SIZE, ACCOUNT_PASSWORD_ITERATIONS,
119  ACCOUNT_PASSWORD_SIZE, account->password);
120 }
121 
122 static int account_check_password(account_struct *account, char *password)
123 {
124  unsigned char output[ACCOUNT_PASSWORD_SIZE];
125 
126  if (account->password_old) {
127  return strcmp(account_old_crypt(password, account->password_old),
128  account->password_old) == 0;
129  }
130 
131  PKCS5_PBKDF2_HMAC_SHA2((const unsigned char *) password, strlen(password),
132  account->salt, ACCOUNT_PASSWORD_SIZE, ACCOUNT_PASSWORD_ITERATIONS,
133  ACCOUNT_PASSWORD_SIZE, output);
134 
135  return memcmp(account->password, output, sizeof(output)) == 0;
136 }
137 
138 static int account_save(account_struct *account, const char *path)
139 {
140  FILE *fp;
141  char hex[ACCOUNT_PASSWORD_SIZE * 2 + 1];
142  size_t i;
143 
144  fp = fopen(path, "w");
145 
146  if (!fp) {
147  LOG(BUG, "Could not open %s for writing.", path);
148  return 0;
149  }
150 
151  if (string_tohex(account->password, ACCOUNT_PASSWORD_SIZE, hex, sizeof(hex),
152  false) == sizeof(hex) - 1) {
153  fprintf(fp, "pswd %s\n", hex);
154  }
155 
156  if (string_tohex(account->salt, ACCOUNT_PASSWORD_SIZE, hex, sizeof(hex),
157  false) == sizeof(hex) - 1) {
158  fprintf(fp, "salt %s\n", hex);
159  }
160 
161  fprintf(fp, "host %s\n", account->last_host);
162  fprintf(fp, "time %"PRIu64 "\n", (uint64_t) account->last_time);
163 
164  for (i = 0; i < account->characters_num; i++) {
165  fprintf(fp, "char %s:%s:%s:%d\n", account->characters[i].at->name, account->characters[i].name, account->characters[i].region_name, account->characters[i].level);
166  }
167 
168  fclose(fp);
169 
170  return 1;
171 }
172 
173 static int account_load(account_struct *account, const char *path)
174 {
175  FILE *fp;
176  char buf[MAX_BUF], *end;
177 
178  fp = fopen(path, "rb");
179 
180  if (!fp) {
181  LOG(BUG, "Could not open %s for reading.", path);
182  return 0;
183  }
184 
185  memset(account, 0, sizeof(*account));
186 
187  while (fgets(buf, sizeof(buf), fp)) {
188  end = strchr(buf, '\n');
189 
190  if (end) {
191  *end = '\0';
192  }
193 
194  if (strncmp(buf, "pswd ", 5) == 0) {
195  size_t len;
196 
197  len = strlen(buf + 5);
198 
199  if (len == 13 || len == 40) {
200  account->password_old = estrdup(buf + 5);
201  } else if (string_fromhex(buf + 5, len, account->password, ACCOUNT_PASSWORD_SIZE) != ACCOUNT_PASSWORD_SIZE) {
202  LOG(BUG, "Invalid password entry in file: %s", path);
203  memset(account->password, 0, sizeof(account->password));
204  }
205  } else if (strncmp(buf, "salt ", 5) == 0) {
206  if (string_fromhex(buf + 5, strlen(buf + 5), account->salt, ACCOUNT_PASSWORD_SIZE) != ACCOUNT_PASSWORD_SIZE) {
207  LOG(BUG, "Invalid salt entry in file: %s", path);
208  memset(account->salt, 0, sizeof(account->salt));
209  }
210  } else if (strncmp(buf, "host ", 5) == 0) {
211  account->last_host = estrdup(buf + 5);
212  } else if (strncmp(buf, "time ", 5) == 0) {
213  account->last_time = atoll(buf + 5);
214  } else if (strncmp(buf, "char ", 5) == 0) {
215  char *cps[4];
216 
217  if (string_split(buf + 5, cps, arraysize(cps), ':') != arraysize(cps)) {
218  LOG(BUG, "Invalid character entry in file: %s", path);
219  continue;
220  }
221 
222  account->characters = erealloc(account->characters, sizeof(*account->characters) * (account->characters_num + 1));
223  account->characters[account->characters_num].at = arch_find(cps[0]);
224  account->characters[account->characters_num].name = estrdup(cps[1]);
225  account->characters[account->characters_num].region_name = estrdup(cps[2]);
226  account->characters[account->characters_num].level = atoi(cps[3]);
227  account->characters_num++;
228  }
229  }
230 
231  fclose(fp);
232 
233  return 1;
234 }
235 
236 static void account_send_characters(socket_struct *ns, account_struct *account)
237 {
238  packet_struct *packet;
239 
240  packet = packet_new(CLIENT_CMD_CHARACTERS, 64, 64);
241 
242  if (account) {
243  size_t i;
244 
245  packet_debug_data(packet, 0, "Account name");
246  packet_append_string_terminated(packet, ns->account);
247  packet_debug_data(packet, 0, "Hostname");
248  packet_append_string_terminated(packet, socket_get_addr(ns->sc));
249  packet_debug_data(packet, 0, "Last hostname");
250  packet_append_string_terminated(packet, account->last_host);
251  packet_debug_data(packet, 0, "Last time");
252  packet_append_uint64(packet, account->last_time);
253 
254  for (i = 0; i < account->characters_num; i++) {
255  packet_debug(packet, 0, "Character #%" PRIu64 ":\n", (uint64_t) i);
256  packet_debug_data(packet, 1, "Archname");
257  packet_append_string_terminated(packet,
258  account->characters[i].at->name);
259  packet_debug_data(packet, 1, "Name");
260  packet_append_string_terminated(packet,
261  account->characters[i].name);
262  packet_debug_data(packet, 1, "Region name");
263  packet_append_string_terminated(packet,
264  account->characters[i].region_name);
265  packet_debug_data(packet, 1, "Animation ID");
266  packet_append_uint16(packet,
267  account->characters[i].at->clone.animation_id);
268  packet_debug_data(packet, 1, "Level");
269  packet_append_uint8(packet, account->characters[i].level);
270  }
271  }
272 
273  socket_send_packet(ns, packet);
274 }
275 
276 char *account_make_path(const char *name)
277 {
278  StringBuffer *sb;
279  size_t i;
280  char *cp;
281 
282  sb = stringbuffer_new();
283  stringbuffer_append_printf(sb, "%s/accounts/", settings.datapath);
284 
285  for (i = 0; i < settings.limits[ALLOWED_CHARS_ACCOUNT][0]; i++) {
286  stringbuffer_append_string_len(sb, name, i + 1);
287  stringbuffer_append_string(sb, "/");
288  }
289 
290  stringbuffer_append_printf(sb, "%s.dat", name);
291  cp = stringbuffer_finish(sb);
292 
293  return cp;
294 }
295 
296 void account_login(socket_struct *ns, char *name, char *password)
297 {
298  account_struct account;
299  char *path;
300 
301  if (ns->account) {
302  ns->state = ST_DEAD;
303  return;
304  }
305 
306  if (*name == '\0' || *password == '\0' || string_contains_other(name, settings.allowed_chars[ALLOWED_CHARS_ACCOUNT]) || string_contains_other(password, settings.allowed_chars[ALLOWED_CHARS_PASSWORD])) {
307  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid name and/or password.");
308  account_send_characters(ns, NULL);
309  return;
310  }
311 
312  string_tolower(name);
313  path = account_make_path(name);
314 
315  if (!path_exists(path)) {
316  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "No such account.");
317  account_send_characters(ns, NULL);
318  efree(path);
319  return;
320  }
321 
322  if (!account_load(&account, path)) {
323  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Read error occurred, please contact server administrator.");
324  account_send_characters(ns, NULL);
325  efree(path);
326  return;
327  }
328 
329  if (!account_check_password(&account, password)) {
330  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid password.");
331  account_send_characters(ns, NULL);
332  account_free(&account);
333  efree(path);
334 
335  ns->password_fails++;
336  LOG(SYSTEM, "%s: Failed to provide correct password for account %s.", socket_get_str(ns->sc), name);
337 
339  LOG(SYSTEM, "%s: Failed to provide a correct password for account %s too many times!", socket_get_str(ns->sc), name);
340  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "You have failed to provide a correct password too many times.");
341  ns->state = ST_ZOMBIE;
342  }
343 
344  return;
345  }
346 
347  if (account.password_old) {
348  account_set_password(&account, password);
349  }
350 
351  ns->account = estrdup(name);
352  account_send_characters(ns, &account);
353 
354  efree(account.last_host);
355  account.last_host = estrdup(socket_get_addr(ns->sc));
356  account.last_time = datetime_getutc();
357  account_save(&account, path);
358  account_free(&account);
359  efree(path);
360 }
361 
362 void account_register(socket_struct *ns, char *name, char *password, char *password2)
363 {
364  size_t name_len, password_len;
365  char *path;
366  account_struct account;
367 
368  if (ns->account) {
369  ns->state = ST_DEAD;
370  return;
371  }
372 
373  if (*name == '\0' || *password == '\0' || *password2 == '\0' || string_contains_other(name, settings.allowed_chars[ALLOWED_CHARS_ACCOUNT]) || string_contains_other(password, settings.allowed_chars[ALLOWED_CHARS_PASSWORD]) || string_contains_other(password2, settings.allowed_chars[ALLOWED_CHARS_PASSWORD])) {
374  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid name and/or password.");
375  return;
376  }
377 
378  name_len = strlen(name);
379  password_len = strlen(password);
380 
381  /* Ensure the name/password lengths are within the allowed range.
382  * No need to compare 'password2' length, as it needs to be the same
383  * as 'password' anyway. */
384  if (name_len < settings.limits[ALLOWED_CHARS_ACCOUNT][0] || name_len > settings.limits[ALLOWED_CHARS_ACCOUNT][1] || password_len < settings.limits[ALLOWED_CHARS_PASSWORD][0] || password_len > settings.limits[ALLOWED_CHARS_PASSWORD][1]) {
385  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid length for name and/or password.");
386  return;
387  }
388 
389  if (strcasecmp(name, ACCOUNT_TESTING_NAME) == 0) {
390  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns,
391  "Account name is reserved by the system.");
392  return;
393  }
394 
395  if (strcmp(password, password2) != 0) {
396  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "The passwords did not match.");
397  return;
398  }
399 
400  string_tolower(name);
401  path = account_make_path(name);
402 
403  if (path_exists(path)) {
404  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "That account name is already registered.");
405  efree(path);
406  return;
407  }
408 
409  path_ensure_directories(path);
410 
411  account_set_password(&account, password);
412  account.last_host = socket_get_addr(ns->sc);
413  account.last_time = datetime_getutc();
414  account.characters = NULL;
415  account.characters_num = 0;
416 
417  if (!account_save(&account, path)) {
418  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Save error occurred, please contact server administrator.");
419  efree(path);
420  return;
421  }
422 
423  ns->account = estrdup(name);
424  account_send_characters(ns, &account);
425  efree(path);
426 }
427 
428 void account_new_char(socket_struct *ns, char *name, char *archname)
429 {
430  archetype_t *at;
431  char *path, *path_player;
432  account_struct account;
433 
434  if (!ns->account) {
435  ns->state = ST_DEAD;
436  return;
437  }
438 
439  if (*name == '\0' || string_contains_other(name, settings.allowed_chars[ALLOWED_CHARS_CHARNAME])) {
440  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid character name");
441  return;
442  }
443 
444  string_title(name);
445 
446  if (strcmp(name, PLAYER_TESTING_NAME1) == 0 ||
447  strcmp(name, PLAYER_TESTING_NAME2) == 0) {
448  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns,
449  "Character name is reserved by the system.");
450  return;
451  }
452 
453  if (player_exists(name)) {
454  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Character with that name already exists.");
455  return;
456  }
457 
458  at = arch_find(archname);
459 
460  if (!at || at->clone.type != PLAYER) {
461  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid archname.");
462  return;
463  }
464 
465  path = account_make_path(ns->account);
466 
467  if (!account_load(&account, path)) {
468  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Read error occurred, please contact server administrator.");
469  efree(path);
470  return;
471  }
472 
473  if (account.characters_num >= ACCOUNT_CHARACTERS_LIMIT) {
474  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "You have reached the maximum number of allowed characters per account.");
475  account_free(&account);
476  efree(path);
477  return;
478  }
479 
480  path_player = player_make_path(name, "player.dat");
481 
482  if (!path_touch(path_player)) {
483  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Write error occurred, please contact server administrator.");
484  account_free(&account);
485  efree(path);
486  efree(path_player);
487  return;
488  }
489 
490  efree(path_player);
491 
492  account.characters = erealloc(account.characters, sizeof(*account.characters) * (account.characters_num + 1));
493  account.characters[account.characters_num].at = at;
494  account.characters[account.characters_num].name = estrdup(name);
495  account.characters[account.characters_num].region_name = estrdup("");
496  account.characters[account.characters_num].level = 1;
497  account.characters_num++;
498 
499  if (!account_save(&account, path)) {
500  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Write error occurred, please contact server administrator.");
501  account_free(&account);
502  efree(path);
503  return;
504  }
505 
506  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_GREEN, ns, "New character created successfully.");
507  account_send_characters(ns, &account);
508  account_free(&account);
509  efree(path);
510 }
511 
512 void account_login_char(socket_struct *ns, char *name)
513 {
514  char *path;
515  account_struct account;
516  size_t i;
517 
518  if (!ns->account) {
519  ns->state = ST_DEAD;
520  return;
521  }
522 
523  path = account_make_path(ns->account);
524 
525  if (!account_load(&account, path)) {
526  efree(path);
527  return;
528  }
529 
530  efree(path);
531 
532  for (i = 0; i < account.characters_num; i++) {
533  if (strcmp(account.characters[i].name, name) == 0) {
534  break;
535  }
536  }
537 
538  if (i == account.characters_num) {
539  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "No such character.");
540  account_free(&account);
541  return;
542  }
543 
544  player_login(ns, name, account.characters[i].at);
545  account_free(&account);
546 }
547 
548 void account_logout_char(socket_struct *ns, player *pl)
549 {
550  char *path;
551  account_struct account;
552  size_t i;
553 
554  path = account_make_path(ns->account);
555 
556  if (!account_load(&account, path)) {
557  efree(path);
558  return;
559  }
560 
561  for (i = 0; i < account.characters_num; i++) {
562  if (strcmp(account.characters[i].name, pl->ob->name) == 0) {
563  efree(account.characters[i].region_name);
564  account.characters[i].region_name = estrdup(pl->ob->map->region ? region_get_longname(pl->ob->map->region) : "???");
565  string_replace_char(account.characters[i].region_name, ":", ' ');
566  account.characters[i].level = pl->ob->level;
567  break;
568  }
569  }
570 
571  account_save(&account, path);
572  account_free(&account);
573  efree(path);
574 }
575 
576 void account_password_change(socket_struct *ns, char *password, char *password_new, char *password_new2)
577 {
578  size_t password_new_len;
579  char *path;
580  account_struct account;
581 
582  if (!ns->account) {
583  ns->state = ST_DEAD;
584  return;
585  }
586 
587  if (*password == '\0' || *password_new == '\0' || *password_new2 == '\0' || string_contains_other(password, settings.allowed_chars[ALLOWED_CHARS_PASSWORD]) || string_contains_other(password_new, settings.allowed_chars[ALLOWED_CHARS_PASSWORD]) || string_contains_other(password_new2, settings.allowed_chars[ALLOWED_CHARS_PASSWORD])) {
588  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid password.");
589  return;
590  }
591 
592  password_new_len = strlen(password_new);
593 
594  if (password_new_len < settings.limits[ALLOWED_CHARS_PASSWORD][0] || password_new_len > settings.limits[ALLOWED_CHARS_PASSWORD][1]) {
595  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid length for password.");
596  return;
597  }
598 
599  if (strcmp(password_new, password_new2) != 0) {
600  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "The new passwords did not match.");
601  return;
602  }
603 
604  path = account_make_path(ns->account);
605 
606  if (!account_load(&account, path)) {
607  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Read error occurred, please contact server administrator.");
608  efree(path);
609  return;
610  }
611 
612  if (!account_check_password(&account, password)) {
613  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Invalid password.");
614  account_free(&account);
615  efree(path);
616  return;
617  }
618 
619  account_set_password(&account, password_new);
620 
621  if (account_save(&account, path)) {
622  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_GREEN, ns, "Password changed successfully.");
623  } else {
624  draw_info_send(CHAT_TYPE_GAME, NULL, COLOR_RED, ns, "Save error occurred, please contact server administrator.");
625  }
626 
627  account_free(&account);
628  efree(path);
629 }
630 
631 void account_password_force(object *op, char *name, const char *password)
632 {
633  size_t password_len;
634  char *path;
635  account_struct account;
636 
637  HARD_ASSERT(op != NULL);
638  HARD_ASSERT(name != NULL);
639  HARD_ASSERT(password != NULL);
640 
641  if (*password == '\0' || string_contains_other(password,
642  settings.allowed_chars[ALLOWED_CHARS_PASSWORD])) {
643  draw_info(COLOR_RED, op, "Invalid password.");
644  return;
645  }
646 
647  password_len = strlen(password);
648 
649  if (password_len < settings.limits[ALLOWED_CHARS_PASSWORD][0] ||
650  password_len > settings.limits[ALLOWED_CHARS_PASSWORD][1]) {
651  draw_info(COLOR_RED, op, "Invalid length for password.");
652  return;
653  }
654 
655  string_tolower(name);
656  path = account_make_path(name);
657 
658  if (!path_exists(path)) {
659  draw_info(COLOR_RED, op, "No such account.");
660  efree(path);
661  return;
662  }
663 
664  if (!account_load(&account, path)) {
665  draw_info(COLOR_RED, op, "Read error occurred, please contact server "
666  "administrator.");
667  efree(path);
668  return;
669  }
670 
671  account_set_password(&account, password);
672 
673  if (account_save(&account, path)) {
674  draw_info(COLOR_GREEN, op, "Password changed successfully.");
675  } else {
676  draw_info(COLOR_RED, op, "Save error occurred, please contact server "
677  "administrator.");
678  }
679 
680  account_free(&account);
681  efree(path);
682 }
socket_t * sc
Definition: newserver.h:109
char datapath[MAX_BUF]
Definition: global.h:343
object * ob
Definition: player.h:185
#define PLAYER
Definition: define.h:122
const char * region_get_longname(const region_struct *region)
Definition: region.c:309
#define MAX_PASSWORD_FAILURES
Definition: newserver.h:242
size_t limits[ALLOWED_CHARS_NUM][2]
Definition: global.h:434
Definition: arch.h:40
void player_login(socket_struct *ns, const char *name, struct archetype *at)
Definition: player.c:2738
uint16_t animation_id
Definition: object.h:322
struct mapdef * map
Definition: object.h:139
const char * name
Definition: object.h:168
char allowed_chars[ALLOWED_CHARS_NUM][MAX_BUF]
Definition: global.h:429
struct settings_struct settings
Definition: init.c:55
uint8_t password_fails
Definition: newserver.h:158
region_struct * region
Definition: map.h:580
uint8_t type
Definition: object.h:360
shstr * name
More definite name, like "kobold".
Definition: arch.h:46
int8_t level
Definition: object.h:347
object clone
An object from which to do object_copy().
Definition: arch.h:47
archetype_t * arch_find(const char *name)
Definition: arch.c:407