Update FileClass code to build and work on Unix. - #145
Conversation
0f549bc to
a1c357d
Compare
|
I've removed the utf.h file that I've originally copied over from Vanilla Conquer. Its purpose was to allow the binary to be compiled in UTF-16 mode, though it appears that the project targets UTF-8 mode instead. I've also cleaned up 2 accidental changes, the removal of DOS date parsing macros (they're restored), and changing permissions to a script file (they're back to normal now). I think the PR is ready for review now. I've tested the code, the RawFileClass behaves outwardly the same to the old version, including the READ|WRITE mode, for which I had to introduce fseek() calls when switching between reads and writes as per the following documentation page: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen?view=msvc-170 The DOS timestamp setting/getting also works on both Windows and Linux now. With these changes, I'm able to locally build the CCFileClass / MixFileClass on Linux and load assets from MIX files, and the game on Windows appears to still work correctly too (cutscenes load, levels load, settings work, saving and loading games works). |
52b97e5 to
3b01d74
Compare
|
I'm attaching the test programs I've been using to test these changes: test_main.cpp - Loads a single file out of a mixfile I've used the first program to verify that the MixFileClass / CCFileClass is able to correctly load assets on Linux, including correctly dealing with differing case sensitivity, and I've used the second program to verify that reading/writing and setting/getting the modification timestamp works as expected, matching the behavior of the old code. |
|
|
||
| default: | ||
| return(path + '\\'); | ||
| return(path + (char)std::filesystem::path::preferred_separator); |
There was a problem hiding this comment.
std::filesystem::path::preferred_separator for Win32 build is L'\\'?
There was a problem hiding this comment.
That is correct, here is the reference page that I've followed: https://en.cppreference.com/cpp/filesystem/path/make_preferred
| bool Find_File_Data_Posix::FindNextWithFilter() | ||
| { | ||
| while (true) { | ||
| DirEntry = readdir(Directory); | ||
| if (DirEntry == nullptr) { | ||
| return false; | ||
| } | ||
| if (fnmatch(FileFilter, DirEntry->d_name, FNM_PATHNAME | FNM_CASEFOLD) == 0) { | ||
| strcpy(FullName, DirName); | ||
| strcat(FullName, DirEntry->d_name); | ||
| break; | ||
| } | ||
| } | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Why not use methods from std::filesystem and less dangrous functions like strcpy?
There was a problem hiding this comment.
Good point! The file* module is copied as-is from Vanilla Conquer, but I can certainly spend some time updating it to be more idiomatic.
There was a problem hiding this comment.
Good point! The file* module is copied as-is from Vanilla Conquer, but I can certainly spend some time updating it to be more idiomatic.
I might be wrong, but Vanilla Conquer better to comment with permalink to the github repo.
| ULARGE_INTEGER ull; | ||
| ull.LowPart = FindData.ftLastWriteTime.dwLowDateTime; | ||
| ull.HighPart = FindData.ftLastWriteTime.dwHighDateTime; | ||
| return (unsigned int)(ull.QuadPart / 10000000ULL - 11644473600ULL); |
| #include <utime.h> | ||
| #include <ctime> | ||
| #define _unlink unlink | ||
| #define fopen(x, y) fopen(x, y) |
There was a problem hiding this comment.
Good point! Thanks for catching this leftover, it's from when there was a raw_fopen() wrapper call for dealing with a potential UTF-16 build, that's been removed though with the removal of the utf.h file. I'll clean this up.
| if (fclose(Handle) != 0) { | ||
| Error(errno, false, Filename); |
There was a problem hiding this comment.
| if (fclose(Handle) != 0) { | |
| Error(errno, false, Filename); | |
| if (!fclose(Handle)) { | |
| Error(errno, false, Filename); |
There was a problem hiding this comment.
I prefer having != 0 here to emphasize that the success condition is zero. This is because not including that comparison operator makes it look like a typical boolean operation where zero is failure and non-zero is success, but it's the other way around for this call - and your suggestion introduces a bug :)
| Date = (((parsed_time->tm_year - 80) & ((1 << 7) - 1)) << 9) | | ||
| (((parsed_time->tm_mon + 1) & ((1 << 4) - 1)) << 5) | | ||
| (parsed_time->tm_mday & ((1 << 5) - 1)); |
There was a problem hiding this comment.
The data format is defined here: https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-dosdatetimetovarianttime
Would you like me to introduce macros to make this clearer, or perhaps explanation comments?
There was a problem hiding this comment.
The header file rawfile.h also contains comments that describe the dos date/time bitfields
There was a problem hiding this comment.
But yeah, the way the bitshifts work is that ((1 << n) - 1) creates an n-bit mask of all 1s, used for ANDing (picking out the relevant bits from the input), and the subsequent << n shifts the selected bit range into the correct position as defined in the spec.
| Time = ((parsed_time->tm_hour & ((1 << 5) - 1)) << 11) | | ||
| ((parsed_time->tm_min & ((1 << 6) - 1)) << 5) | | ||
| ((parsed_time->tm_sec >> 1) & ((1 << 5) - 1)); | ||
|
|
||
| return(Date << 16 | Time); |
| Date = (datetime >> 16) & 0xFFFF; | ||
| Time = datetime & 0xFFFF; | ||
|
|
||
| input_time.tm_year = ((Date >> 9) & ((1 << 7) - 1)) + 80; | ||
| input_time.tm_mon = ((Date >> 5) & ((1 << 4) - 1)) - 1; | ||
| input_time.tm_mday = Date & ((1 << 5) - 1); | ||
|
|
||
| input_time.tm_hour = (Time >> 11) & ((1 << 5) - 1); | ||
| input_time.tm_min = (Time >> 5) & ((1 << 6) - 1); | ||
| input_time.tm_sec = (Time & ((1 << 5) - 1)) << 1; | ||
|
|
||
| input_time.tm_isdst = -1; |
| ** the staging buffer. | ||
| */ | ||
| int add_count = std::min((int)length, SRC_BLOCK_SIZE - PartialCount); | ||
| int add_count = std::min((int)length, (int)SRC_BLOCK_SIZE - PartialCount); |
There was a problem hiding this comment.
Maybe better static_cast at least with std::min?
There was a problem hiding this comment.
Okay, I'll play around with it. I added the cast because I got an overloading resolution failure when trying to build the original line with g++
There was a problem hiding this comment.
Okay, I'll play around with it. I added the cast because I got an overloading resolution failure when trying to build the original line with g++
I'm not the project maintainer or contributor, but I see the danger in using C-casts in C++ code base instead of CPP-casts. The maintainers' opinions may differ from mine.
| #define _MAX_FNAME 255 | ||
| #define _MAX_EXT 8 | ||
| #define _MAX_PATH 512 |
There was a problem hiding this comment.
I have a lot of questions about this block because Win 10 support much longer paths
https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry
There was a problem hiding this comment.
Yeah, that's true, but like the linked document page suggests, in order to make use of the extended path lengths, you need to prefix them with "\?".
I think that enabling long paths is going to be a long-term refactoring effort outside the scope of this particular pull request. I can certainly start investigating it though
There was a problem hiding this comment.
Yeah, that's true, but like the linked document page suggests, in order to make use of the extended path lengths, you need to prefix them with "?".
I think that enabling long paths is going to be a long-term refactoring effort outside the scope of this particular pull request. I can certainly start investigating it though
Sorry, but I worried if the next OTS build would be worse than official game.exe because game can crash or if there are non-ascii chars in path.
There was a problem hiding this comment.
Only on < Windows 10 Version 1903 (May 2019 Update
|
|
||
| default: | ||
| UserPath += '\\'; | ||
| UserPath += std::filesystem::path::preferred_separator; |
There was a problem hiding this comment.
It's std::string, it's defined at the start of the cdfile.c file
There was a problem hiding this comment.
It's
std::string, it's defined at the start of thecdfile.cfile
Would you kindly check, is UserPath are wchat_t with win32 build? Because std::filesystem::path::preferred_separator is a wchat_t for win32 and char for linux.
There was a problem hiding this comment.
No, UserPath is not wchar_t, it's a regular 8-bit character string. The OpenTS project uses UTF-8 internally, and it has it configured in the manifest, so it should be fine to use the "narrow" APIs instead of the wide APIs, passing them 8-bit character strings.
The preferred_separator is realistically only going to have values in the ASCII range, either forward-slash or backslash, perhaps a colon on Classic MacOS but I don't see there being a C++20 compiler for that, so it should be fine to down-cast it from wchar_t to char.
| #ifndef _WIN32 | ||
| // step through each sub-directory before going for the win | ||
| char* next = fname; | ||
| while (next = strchr(next, '/')) { | ||
| *next = '\0'; | ||
| Resolve_File_Single(fname); | ||
| *next++ = '/'; | ||
| } | ||
|
|
||
| Resolve_File_Single(fname); | ||
| #endif |
There was a problem hiding this comment.
Are you sure that type of magic is a good one?
There was a problem hiding this comment.
It's been tested under Vanilla Conquer, that's where I got it from. But it is true that since we have C++20 enabled, I should probably investigate how to rewrite this cleaner with std::filesystem
There was a problem hiding this comment.
It's been tested under Vanilla Conquer, that's where I got it from. But it is true that since we have C++20 enabled, I should probably investigate how to rewrite this cleaner with
std::filesystem
With std::filesystem it would be much cleaner and CPPish
| #define _MAX_EXT 8 | ||
| #define _MAX_PATH 512 | ||
| #define MAX_PATH _MAX_PATH | ||
| #define _CONTROL 0x20 // space, first non-control character in ASCII |
There was a problem hiding this comment.
It delimits the range of ASCII control characters vs printable characters. It's an internal Microsoft ctype.h thing that Westwood ended up using in their code. Should be factored out, not in the scope of this PR
| #undef _stricmp | ||
| #define stricmp strcasecmp | ||
| #define _stricmp strcasecmp | ||
| #define strnicmp strncasecmp | ||
| #define memicmp strncasecmp | ||
| #define __cdecl |
There was a problem hiding this comment.
What magic is this? Also why #define __cdecl
There was a problem hiding this comment.
__cdecl doesn't exist on some compilers.
| } | ||
| *ffblk = nullptr; | ||
|
|
||
| *ffblk = Find_File_Data::CreateFindData(); |
There was a problem hiding this comment.
Could Find_File_Data::CreateFindData() return nullptr?
| delete *ffblk; | ||
| *ffblk = nullptr; |
There was a problem hiding this comment.
God bless smart pointers. I wish OTS would use them.
There was a problem hiding this comment.
It does, where appropriate. Smart pointers are not a panacea
| #ifndef _WIN32 | ||
| #ifndef _GNU_SOURCE | ||
| #define _GNU_SOURCE | ||
| #endif |
There was a problem hiding this comment.
Why #endif in the start of thee file if this file are *nix only?
There was a problem hiding this comment.
Because we have cmake glob files so this will also get built on other platforms
| int position = ftell(Handle); | ||
| if (position < 0) { | ||
| Error(errno, false, Filename); | ||
| return(0); |
|
|
||
| if (fseek(Handle, 0, SEEK_END) < 0) { | ||
| Error(errno, false, Filename); | ||
| return(0); |
There was a problem hiding this comment.
Because Westwood devs apparently liked using parenthesis around the return values.
I'm trying to keep the style of the code the same as the remaining fine, there's nothing more jarring than having multiple different coding styles within a single file :P
| WORD dosdate; | ||
| WORD dostime; | ||
| FileTimeToDosDateTime(&info.ftLastWriteTime, &dosdate, &dostime); | ||
| return((dosdate << 16) | dostime); |
There was a problem hiding this comment.
The DOS date/time format is described in the header files, there are macros provided at the top of wwfile.h to parse out the fields, and the bit ranges are described in the class declaration in rawfile.h.
I've added links to Microsoft and Single UNIX Specification docs for the corresponding date/time formats into the functions.
| buf.actime = unix_time; | ||
| buf.modtime = unix_time; | ||
|
|
||
| return(utime(Filename, &buf) == 0); |
There was a problem hiding this comment.
tbf this syntax of return feels like C99, not C++20.
There was a problem hiding this comment.
The thing with return having parenthesis is a Westwoodism, I'm trying to match the code to its surroundings.
| if (fseek(Handle, pos, dir) < 0) { | ||
| Error(errno, false, Filename); | ||
| } | ||
| LastAccessType = 0; |
There was a problem hiding this comment.
It's not a magic number, it's the same data type as the read/write permission arguments to Open(), the used values are READ, WRITE, and 0 for there being no previous I/O operation.
|
This is gonna need a bit of an update once the save file format rework lands, because it replaces CLSID with a local ClassID copy, and thus the INI methods will need to stay. |
3b01d74 to
8c5a34c
Compare
|
Thanks for the heads-up, I'll rebase the changes tomorrow, and hopefully also add a change description. In the meantime, I've updated the copyright notices, added comments with links to docs about the date/time format, and updated the failing test to perform case-insensitive string matching. I've used _stricmp() for this, which appears to be used throughout the game for this purpose, but if you'd prefer a different way to do this, I'll happily update the code again! |
This pull request introduces updates from Vanilla Conquer to the FileClass classes to make them work on Unix / POSIX systems.
It introduces a file.cpp module from Vanilla Conquer that allows for searching/identifying files in the filesystem. This replaces the file finding functions in CDFileClass, and it should be possible to replace the other file finding code in the project with them.
The utf.h header from Vanilla Conquer is also introduced. Perhaps it could be merged with the newly-introduced utf8.h header?