File handling and string manipulation has always been a blindspot for me, as far as C is concerned. Basically, I've got a textfile that contains information on items within my game. I want to store this information as a plain textfile so that other people can edit it easily.
Here's some messy code that does work, but I feel there are a few problems with it. My first question: when using fgets, is it safe to just have a global buffer size (1000), like the one I'm using already? Or should I be specifying dependant on the string I'm grabbing (name, desc, etc). Is fgets even the best solution?
Also: the strings copy with a strange character at the end (^). I'm assuming this is is because the string isn't null terminated. But as I stated earlier, I really want to create and edit this file via a text editor (read: Notepad). What can I do?
An example file layout:
The item name. Some fantastic description! 1
Some code:
| 1 | |
| 2 | typedef struct { |
| 3 | |
| 4 | int stackable; |
| 5 | char name[256], desc[512]; |
| 6 | |
| 7 | } Item; |
| 8 | |
| 9 | Item items[max_item_num]; |
| 10 | |
| 11 | int setup_objects( char *filename ) { |
| 12 | |
| 13 | FILE *f = fopen(filename,"r"); |
| 14 | char i_string[1000]; |
| 15 | int item_num = 0; |
| 16 | |
| 17 | if (!f) return 0; |
| 18 | |
| 19 | while ( !feof(f) ) { |
| 20 | |
| 21 | if( fgets(i_string,1000,f) != NULL ) { |
| 22 | sprintf(items[item_num].name, i_string); |
| 23 | } |
| 24 | if( fgets(i_string,1000,f) != NULL ) { |
| 25 | sprintf(items[item_num].desc, i_string); |
| 26 | } |
| 27 | if( fgets(i_string,1000,f) != NULL ) { |
| 28 | items[item_num].stackable = atoi(i_string); |
| 29 | } |
| 30 | |
| 31 | item_num++; |
| 32 | |
| 33 | } |
| 34 | fclose(f); |
| 35 | |
| 36 | return 1; |
| 37 | |
| 38 | } |
My first question: when using fgets, is it safe to just have a global buffer size (1000), like the one I'm using already?
The fgets is safe, but the sprintf copying isn't. If you're just going to copy a string, use strncpy instead of sprintf (and if you actually do need sprintf's functionality, use snprintf)!
But why not just fgets (with the proper length) directly to the name and desc arrays instead of to a temporary buffer that you copy?
Also: the strings copy with a strange character at the end (^).
That's an LF, as in a CRLF line ending. Open your file in text mode and it should go away. I.e.:FILE *f = fopen(filename, "rt");
Some good points, some updated code. Changing to "rt" hasn't solved the character problem, though.
| 1 | int setup_objects( char *filename ) { |
| 2 | |
| 3 | FILE *f = fopen(filename,"rt"); |
| 4 | char i_string[100]; |
| 5 | int item_num = 0; |
| 6 | |
| 7 | if (!f) return 0; |
| 8 | |
| 9 | while ( !feof(f) ) { |
| 10 | |
| 11 | fgets(items[item_num].name,256,f); |
| 12 | fgets(items[item_num].desc,512,f); |
| 13 | |
| 14 | if( fgets(i_string,1,f) != NULL ) { |
| 15 | items[item_num].stackable = atoi(i_string); |
| 16 | } |
| 17 | |
| 18 | item_num++; |
| 19 | |
| 20 | } |
| 21 | fclose(f); |
| 22 | |
| 23 | return 1; |
| 24 | |
| 25 | } |
mingw fgets() acts just like in Unix. It only recognises \n as EOL, and so when you read text files with \r\n endings, you get a stray \r at the end.
The following code will strip these.
This code is safe with ASCII and with UTF-8, afaics
mingw fgets() acts just like in Unix. It only recognises \n as EOL, and so when you read text files with \r\n endings, you get a stray \r at the end.
Actually, they read up to and including the line-ending. If the line-ends in the file are \r\n, then you'll have \r\n at the end of the string. If it's just \n, you'll have \n at the end. Opening the file in non-b mode should automatically change \r\n to just \n, though.
Still not having much luck with it
Maybe you could try the Allegro config routines, here is an example how it could look like (should compile as C++, don't know about C):
| 1 | typedef struct { |
| 2 | int stackable; |
| 3 | char name[256], desc[512]; |
| 4 | } Item; |
| 5 | |
| 6 | |
| 7 | Item items[max_item_num]; |
| 8 | |
| 9 | |
| 10 | int setup_objects(char *filename) |
| 11 | { |
| 12 | if (!exists(filename)) |
| 13 | return 0; |
| 14 | |
| 15 | push_config_state(); |
| 16 | set_config_file(filename); |
| 17 | |
| 18 | /* |
| 19 | ** This guarantees, that non-ASCII section names will be loaded correctly. |
| 20 | ** You probably want to edit this if you're using only ASCII files and characters. |
| 21 | */ |
| 22 | const size_t section_size = uwidth_max(U_CURRENT) * 16; |
| 23 | char section[section_size]; |
| 24 | |
| 25 | bool at_least_one_loaded = false; |
| 26 | int item_num = 0; |
| 27 | uszprintf(section, section_size, "ITEM_%i", item_num); |
| 28 | |
| 29 | while ((item_num < max_item_num) && config_is_hooked(section)) |
| 30 | { |
| 31 | ustrzncpy(items[item_num].name, sizeof(items[item_num].name), get_config_string(section, "name", empty_string), 255); |
| 32 | ustrzncpy(items[item_num].desc, sizeof(items[item_num].desc), get_config_string(section, "desc", empty_string), 511); |
| 33 | items[item_num].stackable = get_config_int(section, "stackable", 0); |
| 34 | |
| 35 | at_least_one_loaded = true; |
| 36 | item_num++; |
| 37 | uszprintf(section, section_size, "ITEM_%i", item_num); |
| 38 | } |
| 39 | |
| 40 | pop_config_state(); |
| 41 | |
| 42 | /* |
| 43 | ** Returns 1 if at least one item has been loaded, else return 0. |
| 44 | ** Maybe a better way would be to return the number of items loaded? |
| 45 | ** |
| 46 | ** return at_least_one_loaded ? item_num + 1 : 0; |
| 47 | */ |
| 48 | return at_least_one_loaded; |
| 49 | } |
Now your text file should be something like that:
[ITEM_0] name = The item name. desc = Some fantastic description! stackable = 1 [ITEM_1] name = Some other item. desc = Some another fantastic description! stackable = 0
Sorry, but code I've posted isn't tested, but I hope that it will be useful somehow 
EDITED few times.
The code doesn't work for me. I have a question, though:
config_is_hooked(section)
Where do we actually hook the section?
Ups, sorry Nial. I've used wrong function. I wanted to check if a section exists in a file. It seems that Allegro doesn't have such a function, but you may try the
int list_config_sections(const char ***names);
function, but you will need of course to change the source code I've posted greatly.
Tomasz, not a problem! I've used the Allegro config routines once before (long ago). So I'll go RTFM. Thanks for taking the time to type all that up!
Edit: Tomasz, you're right! How the heck can I check (reliably) if a section is not found? I could compare the item name with empty_string, I suppose.
I've wrote a fixed version, this time using the int list_config_sections(const char ***names); function (it's introduced in the version 4.2.1 of Allegro). Also this time it compiles as C++, so you will need to change it in few places to use it as C.
| 1 | typedef struct |
| 2 | { |
| 3 | int stackable; |
| 4 | char name[256], desc[512]; |
| 5 | } Item; |
| 6 | |
| 7 | |
| 8 | Item items[max_item_num]; |
| 9 | |
| 10 | |
| 11 | int setup_objects(char *filename) |
| 12 | { |
| 13 | if (!exists(filename)) |
| 14 | return 0; |
| 15 | |
| 16 | push_config_state(); |
| 17 | set_config_file(filename); |
| 18 | |
| 19 | char const **sections = 0; |
| 20 | int sections_num = list_config_sections(§ions); |
| 21 | int i = 0; |
| 22 | |
| 23 | const char * const item_section_name = "ITEM_"; |
| 24 | const int item_section_name_length = ustrlen(item_section_name); |
| 25 | |
| 26 | for (int s = 0; (s < sections_num) && (i < max_item_num); s++) |
| 27 | if (!ustrnicmp(sections[s], item_section_name, item_section_name_length)) |
| 28 | { |
| 29 | ustrzncpy(items<i>.name, sizeof(items<i>.name), get_config_string(sections[s], "name", empty_string), 255); |
| 30 | ustrzncpy(items<i>.desc, sizeof(items<i>.desc), get_config_string(sections[s], "desc", empty_string), 511); |
| 31 | items<i>.stackable = get_config_int(sections[s], "stackable", 0); |
| 32 | i++; |
| 33 | } |
| 34 | |
| 35 | free_config_entries(§ions); |
| 36 | pop_config_state(); |
| 37 | return i; |
| 38 | } |
Now your text file could look like this:
[ITEM_SWORD] name = Magic Sword desc = This is some text about the Magic Sword. stackable = 1 [ITEM_GUN] name = Big F*cking Gun desc = This is some text about BFG. stackable = 0
I hope that this time it will work for you. You could try to tweak this function, maybe even change it, so it will allocate the items[] array dynamically for you
When I use fgets, I make the buffer size and the bytes requested slightly larger than a MAX_STRING_SIZE #define, which is largest allowed string. Try a 1000 char line in a C file and try to compile it!
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | #define MAX_STRING_SIZE 255 |
| 5 | #define MAX_BUFF_SIZE MAX_STRING_SIZE+1 |
| 6 | |
| 7 | int main(void) |
| 8 | { |
| 9 | FILE *fp; |
| 10 | int linenum = 0; |
| 11 | char text[MAX_BUFF_SIZE]; |
| 12 | |
| 13 | fp = fopen("mytext.txt","rb"); //never had prob with rb myself |
| 14 | if(!fp){ fprintf(stderr,"can't open 'mytext.txt'"); return -1; |
| 15 | while(fgets(text,MAX_BUFF_SIZE,fp)) { |
| 16 | linenum++; |
| 17 | if(MAX_STRING_SIZE < strlen(text)) { |
| 18 | fprintf(stderr,"Max string length exceeded on line %d",linenum); |
| 19 | return -1; |
| 20 | } |
| 21 | do_something_with_line(); |
| 22 | } |
| 23 | fclose(fp); |
| 24 | return 0; |
| 25 | } |