From 0d88c24db65b59aa222bba237c0ecb9f3efa7561 Mon Sep 17 00:00:00 2001 From: Matt Mascarenhas Date: Sun, 30 May 2021 19:39:47 +0100 Subject: [PATCH] cinera: Upgrade to hmmlib2 Features: User-configurable roles and credits Handle SIGINT to quit cleanly and avoid database corruption Fixes: Filesystem event monitoring handles directory creation / deletion Fixed buffer overflow when trying to curl in a non-existent quote --- cinera/cinera.c | 1313 +++++++++++++++++++++++++++++++--------- cinera/cinera_config.c | 615 +++++++++++++++++-- 2 files changed, 1580 insertions(+), 348 deletions(-) diff --git a/cinera/cinera.c b/cinera/cinera.c index e803aa2..8f76dbf 100644 --- a/cinera/cinera.c +++ b/cinera/cinera.c @@ -3,10 +3,10 @@ if [ $(command -v ctime 2>/dev/null) ]; then ctime -begin ${0%.*}.ctm fi -#gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl -gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl -#clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl -#clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl +#gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl +gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl +#clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl +#clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl if [ $(command -v ctime 2>/dev/null) ]; then ctime -end ${0%.*}.ctm @@ -23,13 +23,12 @@ typedef struct version CINERA_APP_VERSION = { .Major = 0, .Minor = 8, - .Patch = 11 + .Patch = 12 }; #include // NOTE(matt): varargs #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #include // NOTE(matt): calloc, malloc, free -#include "hmmlib.h" #include // NOTE(matt): getopts #include #include @@ -44,8 +43,19 @@ version CINERA_APP_VERSION = { #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW #include // NOTE(matt): open() +#undef __USE_XOPEN2K8 + #define __USE_XOPEN2K // NOTE(matt): readlink() #include // NOTE(matt): sleep() +#undef __USE_XOPEN2K + +#define __USE_POSIX +#include // NOTE(matt): sigaction() and sigemptyset() +#undef __USE_POSIX + +#define HMMLIB_IMPLEMENTATION +#include "hmmlib.h" +#define HMMLIB_MAJOR_VERSION 2 // TODO(matt): Remove this after testing #define STB_IMAGE_IMPLEMENTATION #define STBI_NO_LINEAR @@ -118,10 +128,31 @@ clock_t TIMING_START; #define FreeAndResetCount(M, Count) { Free(M); Count = 0; } #define MIN(A, B) A < B ? A : B +#define MAX(A, B) A > B ? A : B +#define Clamp(EndA, N, EndB) int Min = MIN(EndA, EndB); int Max = MAX(EndA, EndB); if(N < Min) { N = Min; } else if(N > Max) { N = Max; } typedef int32_t hash32; typedef hash32 asset_hash; +typedef struct +{ + union + { + int A; + int Hours; + }; + union + { + int B; + int Minutes; + }; + union + { + int C; + int Seconds; + }; +} v3; + void Clear(void *V, uint64_t Size) { @@ -474,6 +505,7 @@ typedef enum MBT_CONFIG_STRING_ASSOCIATION, MBT_CONFIG_TYPE_FIELD, MBT_CONFIG_TYPE_SPEC, + MBT_CREDIT, MBT_IDENTIFIER, MBT_LANDMARK, MBT_MEDIUM, @@ -484,7 +516,9 @@ typedef enum MBT_PROJECT_PTR, MBT_REF_INFO, MBT_RESOLUTION_ERROR, + MBT_ROLE, MBT_SCOPE_TREE, + MBT_SCOPE_TREE_PTR, MBT_SPEAKER, MBT_STRING, MBT_STRING_PTR, @@ -797,6 +831,7 @@ PrintBook(memory_book *M) case MBT_CONFIG_STRING_ASSOCIATION: Assert(0); break; case MBT_CONFIG_TYPE_FIELD: Assert(0); break; case MBT_CONFIG_TYPE_SPEC: Assert(0); break; + case MBT_CREDIT: Assert(0); break; case MBT_IDENTIFIER: Assert(0); break; case MBT_LANDMARK: Assert(0); break; case MBT_MEDIUM: Assert(0); break; @@ -807,7 +842,9 @@ PrintBook(memory_book *M) case MBT_PROJECT_PTR: Assert(0); break; case MBT_REF_INFO: Assert(0); break; case MBT_RESOLUTION_ERROR: Assert(0); break; + case MBT_ROLE: Assert(0); break; case MBT_SCOPE_TREE: Assert(0); break; + case MBT_SCOPE_TREE_PTR: Assert(0); break; case MBT_SPEAKER: Assert(0); break; case MBT_STRING_PTR: Assert(0); break; case MBT_SUPPORT: Assert(0); break; @@ -1144,6 +1181,22 @@ ReadFileIntoBuffer(file *F) return Result; } +char * +ReadFileIntoMemory0(FILE *F) +{ + char *Result = 0; + if(F) + { + fseek(F, 0, SEEK_END); + int Length = ftell(F); + Result = malloc(Length + 1); + fseek(F, 0, SEEK_SET); + fread(Result, Length, 1, F); + Result[Length] = 0; + } + return Result; +} + char *ColourStrings[] = { "\e[0m", @@ -1175,10 +1228,10 @@ Colourise(colour_code C) fprintf(stderr, "%s", ColourStrings[C]); } -void +int PrintString(string S) { - fprintf(stderr, "%.*s", (int)S.Length, S.Base); + return fprintf(stderr, "%.*s", (int)S.Length, S.Base); } void @@ -1197,12 +1250,13 @@ PrintStringI(string S, uint64_t Indentation) PrintString(S); } -void +int PrintStringC(colour_code Colour, string String) { Colourise(Colour); - PrintString(String); + int Result = PrintString(String); Colourise(CS_END); + return Result; } void @@ -1250,6 +1304,7 @@ typedef enum TOKEN_COMMENT_SINGLE, TOKEN_COMMENT_MULTI_OPEN, TOKEN_COMMENT_MULTI_CLOSE, + TOKEN_MINUS, TOKEN_ASSIGN, TOKEN_SEMICOLON, TOKEN_OPEN_BRACE, @@ -1270,6 +1325,7 @@ char *TokenTypeStrings[] = "COMMENT_SINGLE", "COMMENT_MULTI_OPEN", "COMMENT_MULTI_CLOSE", + "MINUS", "ASSIGN", "SEMICOLON", "OPEN_BRACE", @@ -1289,6 +1345,7 @@ char *TokenStrings[] = "//", "/*", "*/", + "-", "=", ";", "{", @@ -1378,10 +1435,14 @@ typedef struct char *IdentifierDescription; char *IdentifierDescription_Medium; char *LocalVariableDescription; + char *IdentifierDescription_Role; + char *IdentifierDescription_Credit; bool IdentifierDescriptionDisplayed; bool IdentifierDescription_MediumDisplayed; bool LocalVariableDescriptionDisplayed; + bool IdentifierDescription_RoleDisplayed; + bool IdentifierDescription_CreditDisplayed; } config_identifier; config_identifier ConfigIdentifiers[] = @@ -1408,6 +1469,11 @@ files for the purpose of hashing them." file credits this person for the entire project. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \ video node of the entries for which they should be credited." }, +#if(HMMLIB_MAJOR_VERSION == 2) + { "credit", "The ID of a person (see also person) who contributed to a project. They will then appear in the credits menu of each entry in the project. Note that setting a credit in the configuration \ +file credits this person for the entire project. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \ +video node of the entries for which they should be credited." }, +#endif { "css_path", "Path relative to assets_root_dir and assets_root_url where CSS files are located." }, { "db_location", "Absolute file path where the database file resides. If you run multiple instances of Cinera on the same machine, please ensure this db_location differs between them." }, { "default_medium", "The ID of a medium (see also medium) which will be the default for the project. May be overridden by setting the medium in the video node of an HMML file." }, @@ -1459,8 +1525,11 @@ default categories are assumed to be topics) or a medium, and both use the same }, { "name", "The person's name as it appears in the credits menu.", -"The name of the medium as it appears in the filter menu and in a tooltip when hovering on the medium's icon in an index item." +"The name of the medium as it appears in the filter menu and in a tooltip when hovering on the medium's icon in an index item.", + 0, +"The name of the role as it appears in the credits menu." }, + { "non-speaking", "We try to abbreviate the names of speakers to cite them more concisely in the timestamps. Set this to \"true\" to prevent abbreviating the names of people in non-speaking roles, and so avoid erroneous clashes." }, { "numbering_scheme", "Possible numbering schemes: \"calendrical\", \"linear\", \"seasonal\". Only \"linear\" (the default) is treated specially. We assume that .hmml file names take the form: \ \"$project$episode_number.hmml\". Under the \"linear\" scheme, Cinera tries to derive each entry's number in its project by skipping past the project ID, then replacing all underscores with full \ @@ -1473,18 +1542,33 @@ stops. This derived number is then used in the search results to denote the entr "The ID of the project's owner." }, { "person", -"This is someone who played a role in the projects, for which they deserve credit (see also: owner, cohost, guest, indexer).", +"This is someone who played a role in the projects, for which they deserve credit (see also: credit).", 0, -"The ID of the person within whose scope the variable occurs." +"The ID of the person within which scope the variable occurs." }, { "player_location", "The location of the project's player pages relative to base_dir and base_url." }, { "player_template", "Path of a HTML template file relative to the templates_dir from which the project's player pages will be generated." }, + { "plural", "The irregular plural form of the role's name. May be left blank for regular plurals (in English, those ending in \"s\")." }, + { "position", +"The position in the credits list in which this role should reside. Positive numbers position the role from the list's start. Negative numbers from the end. Roles lacking a defined position \ +fill the slots left vacant by positioned roles in the order in which they are configured.", + }, { "privacy_check_interval", "In minutes, this sets how often to check if the privacy status of private entries has changed to \"public\"." }, { "project", "The work horse of the whole configuration. A config file lacking project scopes will produce no output. Notably project scopes may themselves contain project scopes.", 0, "The ID of the project within which scope the variable occurs." }, +#if(HMMLIB_MAJOR_VERSION == 2) + { "quote_username", "The username by which insobot recognises a person, for the purpose of retrieving quotes attributed to them." }, +#endif + { "role", +"This is the role for which a person deserves credit (see also: credit).", +0, +0, +0, +"The ID of the role within which scope the variable occurs." + }, { "query_string", "This string (default \"r\") enables web browsers to cache asset files. We hash those files to produce a number, which we then write to HTML files in hexadecimal format, e.g. \ ?r=a59bb130. Hashing may be disabled by setting query_string = \"\";" }, @@ -1492,10 +1576,13 @@ stops. This derived number is then used in the search results to denote the entr { "search_template", "Path of a HTML template file relative to the templates_dir from which the project's search page will be generated." }, { "single_browser_tab", "Setting this to \"true\" (default \"false\") makes the search page open player pages in its own tab." }, { "stream_platform", "This is a setting for the future. We currently only support \"twitch\" but not in any meaningful way." }, +#if(HMMLIB_MAJOR_VERSION == 2) +#else { "stream_username", "We use this username to retrieve quotes from insobot. If it is not set, we use the host's ID when contacting insobot. The purpose of this setting is to let us identify project owners in one way, \ perhaps to automatically construct paths, while recognising that same person when they stream under a different username." }, +#endif { "support", "Information detailing where a person may be supported, to be cited in the credits menu.", 0, @@ -1529,6 +1616,9 @@ typedef enum IDENT_BASE_URL, IDENT_CACHE_DIR, IDENT_COHOST, +#if(HMMLIB_MAJOR_VERSION == 2) + IDENT_CREDIT, +#endif IDENT_CSS_PATH, IDENT_DB_LOCATION, IDENT_DEFAULT_MEDIUM, @@ -1561,20 +1651,30 @@ typedef enum IDENT_LOG_LEVEL, IDENT_MEDIUM, IDENT_NAME, + IDENT_NON_SPEAKING, IDENT_NUMBERING_SCHEME, IDENT_ORIGIN, IDENT_OWNER, IDENT_PERSON, IDENT_PLAYER_LOCATION, IDENT_PLAYER_TEMPLATE, + IDENT_PLURAL, + IDENT_POSITION, IDENT_PRIVACY_CHECK_INTERVAL, IDENT_PROJECT, +#if(HMMLIB_MAJOR_VERSION == 2) + IDENT_QUOTE_USERNAME, +#endif + IDENT_ROLE, IDENT_QUERY_STRING, IDENT_SEARCH_LOCATION, IDENT_SEARCH_TEMPLATE, IDENT_SINGLE_BROWSER_TAB, IDENT_STREAM_PLATFORM, +#if(HMMLIB_MAJOR_VERSION == 2) +#else IDENT_STREAM_USERNAME, +#endif IDENT_SUPPORT, IDENT_SUPPRESS_PROMPTS, IDENT_TEMPLATES_DIR, @@ -1607,6 +1707,15 @@ IndentedCarriageReturn(int IndentationLevel) Indent(IndentationLevel); } +void +AlignText(int Alignment) +{ + for(int i = 0; i < Alignment; ++i) + { + fprintf(stderr, " "); + } +} + void NewParagraph(int IndentationLevel) { @@ -1773,8 +1882,8 @@ typedef enum typedef struct { char *String; - uint64_t Mapping:63; - uint64_t Unused:1; + uint8_t Mapping:6; + uint8_t Unused:2; } config_art_variant; config_art_variant ConfigArtVariants[] = @@ -1954,9 +2063,9 @@ ConfigErrorExpectation(tokens *T, token_type GreaterExpectation, token_type Less } void -IndexingError(string *Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received) +IndexingError(string Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received) { - ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_INDEXING); + ErrorFilenameAndLineNumber(&Filename, LineNumber, Severity, ED_INDEXING); // TODO(matt): Typeset the Message? fprintf(stderr, "%s", Message); @@ -1968,15 +2077,44 @@ IndexingError(string *Filename, uint64_t LineNumber, severity Severity, char *Me } void -IndexingChronologyError(string *Filename, uint64_t LineNumber, char *ThisTimecode, char *PrevTimecode) +PrintTimecode(FILE *Dest, v3 Timecode) +{ + Colourise(CS_BLUE_BOLD); + if(Timecode.Hours) + { + fprintf(Dest, "%i:%02i:%02i", Timecode.Hours, Timecode.Minutes, Timecode.Seconds); + } + else + { + fprintf(Dest, "%i:%02i", Timecode.Minutes, Timecode.Seconds); + } + Colourise(CS_END); +} + +void +IndexingChronologyError(string *Filename, uint64_t LineNumber, +#if(HMMLIB_MAJOR_VERSION == 2) + v3 ThisTimecode, v3 PrevTimecode +#else + char *ThisTimecode, char *PrevTimecode +#endif + ) { severity Severity = S_ERROR; ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_INDEXING); fprintf(stderr, "Timecode "); +#if(HMMLIB_MAJOR_VERSION == 2) + PrintTimecode(stderr, ThisTimecode); +#else PrintC(CS_BLUE_BOLD, ThisTimecode); +#endif fprintf(stderr, " is chronologically earlier than previous timecode ("); +#if(HMMLIB_MAJOR_VERSION == 2) + PrintTimecode(stderr, PrevTimecode); +#else PrintC(CS_BLUE_BOLD, PrevTimecode); +#endif fprintf(stderr, ")\n"); } @@ -3020,6 +3158,7 @@ typedef struct #define AFE 0 // NOTE(matt): Globals +bool GlobalRunning; config *Config; project *CurrentProject; mode Mode; @@ -3249,7 +3388,7 @@ DeclaimBuffer(buffer *Buffer) LogUsage(Buffer); if(PercentageUsed >= 95.0f) { - // TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com + // TODO(matt): Implement either dynamically growing buffers, or phoning home to admin@miblo.net LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed); fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], @@ -3257,7 +3396,7 @@ DeclaimBuffer(buffer *Buffer) } else if(PercentageUsed >= 80.0f) { - // TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com + // TODO(matt): Implement either dynamically growing buffers, or phoning home to admin@miblo.net LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed); fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], @@ -3311,7 +3450,11 @@ typedef struct typedef struct { +#if(HMMLIB_MAJOR_VERSION == 2) + v3 Timecode; +#else char *Timecode; +#endif int Identifier; } identifier; @@ -3445,6 +3588,52 @@ CopyStringToBuffer_(int LineNumber, buffer *Dest, char *Format, ...) Dest->Ptr += Length; } +int +DigitsInTimecode(v3 Timecode) +{ + int Result = 0; + int ColonChar = 1; + if(Timecode.Hours > 0) + { + Result += DigitsInInt(&Timecode.Hours); + Result += ColonChar; + Result += MAX(2, DigitsInInt(&Timecode.Minutes)); + Result += ColonChar; + Result += MAX(2, DigitsInInt(&Timecode.Seconds)); + } + else + { + Result += DigitsInInt(&Timecode.Minutes); + Result += ColonChar; + Result += MAX(2, DigitsInInt(&Timecode.Seconds)); + } + return Result; +} + +#define CopyTimecodeToBuffer(Dest, Timecode) CopyTimecodeToBuffer_(__LINE__, Dest, Timecode) +void +CopyTimecodeToBuffer_(int LineNumber, buffer *Dest, v3 Timecode) +{ + if(DigitsInTimecode(Timecode) + (Dest->Ptr - Dest->Location) >= Dest->Size) + { + fprintf(stderr, "CopyTimecodeToBuffer(%s) call on line %d cannot accommodate timecode:\n", BufferIDStrings[Dest->ID], LineNumber); + PrintTimecode(stderr, Timecode); + fprintf(stderr, "\n"); + __asm__("int3"); + } + else + { + if(Timecode.Hours > 0) + { + CopyStringToBuffer(Dest, "%i:%02i:%02i", Timecode.Hours, Timecode.Minutes, Timecode.Seconds); + } + else + { + CopyStringToBuffer(Dest, "%i:%02i", Timecode.Minutes, Timecode.Seconds); + } + } +} + #define CopyStringToBufferNoFormat(Dest, String) CopyStringToBufferNoFormat_(__LINE__, Dest, String) void CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, string String) @@ -4054,9 +4243,29 @@ InitTemplate(template *Template, string Location, template_type Type) Template->Metadata.NavBuffer = InitBook(MBT_NAVIGATION_BUFFER, 4); } -int -TimecodeToSeconds(char *Timecode) +#define SECONDS_PER_HOUR 3600 +#define SECONDS_PER_MINUTE 60 + +v3 +V3(int A, int B, int C) { + v3 Result = { .A = A, .B = B, .C = C }; + return Result; +} + +int +TimecodeToSeconds( +#if(HMMLIB_MAJOR_VERSION == 2) + v3 Timecode +#else + char *Timecode +#endif + ) +{ + int Result = 0; +#if(HMMLIB_MAJOR_VERSION == 2) + Result = Timecode.Hours * SECONDS_PER_HOUR + Timecode.Minutes * SECONDS_PER_MINUTE + Timecode.Seconds; +#else int HMS[3] = { 0, 0, 0 }; // 0 == Seconds; 1 == Minutes; 2 == Hours int Colons = 0; while(*Timecode) @@ -4083,7 +4292,9 @@ TimecodeToSeconds(char *Timecode) //if(HMS[0] > 59 || HMS[1] > 59 || Timecode[-1] == ':') { return FALSE; } - return HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0]; + Result = HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0]; +#endif + return Result; } typedef struct @@ -4222,9 +4433,10 @@ typedef struct enum { + CreditsError_NoCredentials, CreditsError_NoHost, CreditsError_NoIndexer, - CreditsError_NoCredentials + CreditsError_NoRole, } credits_errors; size_t @@ -6038,8 +6250,8 @@ SortAndAbbreviateSpeakers(speakers *Speakers) This->Abbreviation = InitialString(&Speakers->Abbreviations, Name); } - int Attempt = 0; - while(AbbreviationsClash(&Speakers->Speakers)) + int MaxAttemptCount = 3; + for(int Attempt = 0; Attempt < MaxAttemptCount && AbbreviationsClash(&Speakers->Speakers); ++Attempt) { for(int i = 0; i < Speakers->Speakers.ItemCount; ++i) { @@ -6052,7 +6264,6 @@ SortAndAbbreviateSpeakers(speakers *Speakers) case 2: This->Abbreviation = Name; break; } } - ++Attempt; } } @@ -6287,6 +6498,8 @@ PushConfiguredAssets() PushThemeAssets(); } +// TODO(matt): REMOVE +// char *RoleStrings[] = { "Cohost", @@ -6295,13 +6508,22 @@ char *RoleStrings[] = "Indexer", }; +char *RoleIDStrings[] ={ + "cohost", + "guest", + "host", + "indexer", +}; + typedef enum { R_COHOST, R_GUEST, R_HOST, R_INDEXER, -} role; +} role_id; +// +//// void PushIcon(buffer *Buffer, bool GrowableBuffer, icon_type IconType, string IconString, asset *IconAsset, uint64_t Variants, page_type PageType, bool *RequiresCineraJS) @@ -6414,13 +6636,54 @@ PushIcon(buffer *Buffer, bool GrowableBuffer, icon_type IconType, string IconStr } } -void -PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role Role, bool *RequiresCineraJS) +speaker * +GetSpeaker(memory_book *Speakers, string Username) { - if(Role != R_INDEXER) + speaker *Result = 0; + if(Username.Length > 0) { - speaker *This = MakeSpaceInBook(Speakers); - This->Person = Actor; + for(int i = 0; i < Speakers->ItemCount; ++i) + { + speaker *This = GetPlaceInBook(Speakers, i); + if(!StringsDifferCaseInsensitive(This->Person->ID, Username)) + { + Result = This; + break; + } + } + } + return Result; +} + +void +FreeSpeakers(speakers *Speakers) +{ + FreeBook(&Speakers->Speakers); + FreeBook(&Speakers->Abbreviations); +} + +void +PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, +#if(HMMLIB_MAJOR_VERSION == 2) + role *Role, +#else + role_id Role, +#endif + bool *RequiresCineraJS) +{ + +#if(HMMLIB_MAJOR_VERSION == 2) + if(!Role->NonSpeaking) +#else + if(Role != R_INDEXER) +#endif + { + speaker *This = GetSpeaker(Speakers, Actor->ID); + if(!This) + { + This = MakeSpaceInBook(Speakers); + This->Person = Actor; + } } if(CreditsMenu->Ptr == CreditsMenu->Location) @@ -6440,21 +6703,33 @@ PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role { CopyStringToBuffer(CreditsMenu, " \n" - "
%s
\n" + "
%.*s
\n" "
%.*s
\n" "
\n", (int)Actor->Homepage.Length, Actor->Homepage.Base, +#if(HMMLIB_MAJOR_VERSION == 2) + (int)Role->Name.Length, + Role->Name.Base, +#else + (int)StringLength(RoleStrings[Role]), RoleStrings[Role], +#endif (int)Name.Length, Name.Base); } else { CopyStringToBuffer(CreditsMenu, "
\n" - "
%s
\n" + "
%.*s
\n" "
%.*s
\n" "
\n", +#if(HMMLIB_MAJOR_VERSION == 2) + (int)Role->Name.Length, + Role->Name.Base, +#else + (int)StringLength(RoleStrings[Role]), RoleStrings[Role], +#endif (int)Name.Length, Name.Base); } @@ -6476,11 +6751,23 @@ PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role } void -ErrorCredentials(string HMMLFilepath, string Actor, role Role) +ErrorCredentials(string HMMLFilepath, string Actor, +#if(HMMLIB_MAJOR_VERSION == 2) + role *Role +#else + role_id Role +#endif + ) { ErrorFilenameAndLineNumber(&HMMLFilepath, 0, S_ERROR, ED_INDEXING); - fprintf(stderr, "No credentials for %s%s%s: %s%.*s%s\n", - ColourStrings[CS_YELLOW_BOLD], RoleStrings[Role], ColourStrings[CS_END], + fprintf(stderr, "No credentials for%s%s%.*s%s: %s%.*s%s\n", + ColourStrings[CS_YELLOW_BOLD], +#if(HMMLIB_MAJOR_VERSION == 2) + Role ? " " : "", Role ? (int)Role->Name.Length : 0, Role ? Role->Name.Base : "", +#else + " ", (int)StringLength(RoleStrings[Role]), RoleStrings[Role], +#endif + ColourStrings[CS_END], ColourStrings[CS_MAGENTA_BOLD], (int)Actor.Length, Actor.Base, ColourStrings[CS_END]); fprintf(stderr, "Perhaps you'd like to add a new person to your config file, e.g.:\n" " person = \"%.*s\"\n" @@ -6492,9 +6779,209 @@ ErrorCredentials(string HMMLFilepath, string Actor, role Role) WaitForInput(); } -int -BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metadata, person *Host, speakers *Speakers, bool *RequiresCineraJS) +#if(HMMLIB_MAJOR_VERSION == 2) +void +ErrorRole(string HMMLFilepath, string Role) { + ErrorFilenameAndLineNumber(&HMMLFilepath, 0, S_ERROR, ED_INDEXING); + fprintf(stderr, "No such role: %s%.*s%s\n", + ColourStrings[CS_YELLOW_BOLD], + (int)Role.Length, Role.Base, + ColourStrings[CS_END]); + fprintf(stderr, "Perhaps you'd like to add a new role to your config file, e.g.:\n" + " role = \"%.*s\"\n" + " {\n" + " name = \"Roller\";\n" + " plural = \"Rollae\";\n" + " position = -1;\n" + " }\n", (int)Role.Length, Role.Base); + WaitForInput(); +} +#endif + +typedef enum +{ + MAT_NULL, + MAT_CREDIT, + MAT_CUSTOM, +} misc_attribute_type; + +typedef struct +{ + char *ID; + misc_attribute_type Type; + union { + int CustomIndex; + role_id RoleID; + }; +} misc_attribute; + +// TODO(matt): Migrate away from handling annotator / cohost / guest here +misc_attribute MiscAttributes[] = { + { "annotator", MAT_CREDIT, { R_INDEXER } }, + { "co-host", MAT_CREDIT, { R_COHOST } }, + { "guest", MAT_CREDIT, { R_GUEST } }, + { "custom0", MAT_CUSTOM, { 0 } }, + { "custom1", MAT_CUSTOM, { 1 } }, + { "custom2", MAT_CUSTOM, { 2 } }, + { "custom3", MAT_CUSTOM, { 3 } }, + { "custom4", MAT_CUSTOM, { 4 } }, + { "custom5", MAT_CUSTOM, { 5 } }, + { "custom6", MAT_CUSTOM, { 6 } }, + { "custom7", MAT_CUSTOM, { 7 } }, + { "custom8", MAT_CUSTOM, { 8 } }, + { "custom9", MAT_CUSTOM, { 9 } }, + { "custom10", MAT_CUSTOM, { 10 } }, + { "custom11", MAT_CUSTOM, { 11 } }, + { "custom12", MAT_CUSTOM, { 12 } }, + { "custom13", MAT_CUSTOM, { 13 } }, + { "custom14", MAT_CUSTOM, { 14 } }, + { "custom15", MAT_CUSTOM, { 15 } }, +}; + +#if(HMMLIB_MAJOR_VERSION == 2) +misc_attribute * +GetMiscAttribute(HMML_VideoCustomMetaData *A) +{ + misc_attribute *Result = 0; + for(int i = 0; i < ArrayCount(MiscAttributes); ++i) + { + misc_attribute *This = MiscAttributes + i; + if(!StringsDiffer0(This->ID, A->key)) + { + Result = This; + break; + } + } + return Result; +} +#endif + +bool +CreditsMatch(credit *A, credit *B) +{ + return A->Person == B->Person && A->Role == B->Role; +} + + +credit * +GetCredit(memory_book *Credits, credit *C) +{ + credit *Result = 0; + for(int i = 0; i < Credits->ItemCount; ++i) + { + credit *This = GetPlaceInBook(Credits, i); + if(CreditsMatch(C, This)) + { + Result = This; + break; + } + } + return Result; +} + +#if(HMMLIB_MAJOR_VERSION == 2) +bool +Uncredited(HMML_VideoMetaData *Metadata, credit *C) +{ + bool Result = FALSE; + for(int i = 0; i < Metadata->uncredit_count; ++i) + { + HMML_Credit *This = Metadata->uncredits + i; + if(StringsMatch(C->Person->ID, Wrap0(This->name)) && StringsMatch(C->Role->ID, Wrap0(This->role))) + { + Result = TRUE; + break; + } + } + return Result; +} + +void +MergeCredits(memory_book *Credits, project *CurrentProject, HMML_VideoMetaData *Metadata) +{ + // TODO(matt): Sort by SortName + for(int ProjectCreditIndex = 0; ProjectCreditIndex < CurrentProject->Credit.ItemCount; ++ProjectCreditIndex) + { + credit *Credit = GetPlaceInBook(&CurrentProject->Credit, ProjectCreditIndex); + if(!Uncredited(Metadata, Credit) && !GetCredit(Credits, Credit)) + { + credit *New = MakeSpaceInBook(Credits); + New->Person = Credit->Person; + New->Role = Credit->Role; + } + } + + for(int CreditIndex = 0; CreditIndex < Metadata->credit_count; ++CreditIndex) + { + HMML_Credit *This = Metadata->credits + CreditIndex; + credit Credit = {}; + Credit.Person = GetPersonFromConfig(Wrap0(This->name)); + Credit.Role = GetRoleByID(Config, Wrap0(This->role)); + if(!Uncredited(Metadata, &Credit) && !GetCredit(Credits, &Credit)) + { + credit *New = MakeSpaceInBook(Credits); + New->Person = Credit.Person; + New->Role = Credit.Role; + } + } + + // TODO(matt): Delete this loop once we're ready to deprecate the old annotator=person style of crediting + // + for(int CustomIndex = 0; CustomIndex < Metadata->custom_count; ++CustomIndex) + { + HMML_VideoCustomMetaData *This = Metadata->custom + CustomIndex; + misc_attribute *ThisAttribute = GetMiscAttribute(This); + if(ThisAttribute && ThisAttribute->Type == MAT_CREDIT) + { + credit Credit = {}; + Credit.Person = GetPersonFromConfig(Wrap0(This->value)); + Credit.Role = GetRoleByID(Config, Wrap0(RoleIDStrings[ThisAttribute->RoleID])); + if(!Uncredited(Metadata, &Credit) && !GetCredit(Credits, &Credit)) + { + credit *New = MakeSpaceInBook(Credits); + New->Person = Credit.Person; + New->Role = Credit.Role; + } + } + } +} +#endif + +int +BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metadata, +#if(HMMLIB_MAJOR_VERSION == 2) +#else + person *Host, +#endif + speakers *Speakers, bool *RequiresCineraJS) +{ +#if(HMMLIB_MAJOR_VERSION == 2) + memory_book Credits = InitBook(MBT_CREDIT, 4); + MergeCredits(&Credits, CurrentProject, Metadata); + for(int RoleIndex = 0; RoleIndex < Config->Role.ItemCount; ++RoleIndex) + { + role *Role = GetPlaceInBook(&Config->Role, RoleIndex); + + for(int CreditIndex = 0; CreditIndex < Credits.ItemCount; ++CreditIndex) + { + credit *This = GetPlaceInBook(&Credits, CreditIndex); + if(!StringsDiffer(Role->ID, This->Role->ID)) + { + PushCredentials(CreditsMenu, &Speakers->Speakers, This->Person, Role, RequiresCineraJS); + } + } + } + FreeBook(&Credits); + + // NOTE(matt): As we only cite the speaker when there are a multiple of them, we only need to SortAndAbbreviateSpeakers() + // in the same situation + if(Speakers->Speakers.ItemCount > 1) + { + SortAndAbbreviateSpeakers(Speakers); + } + +#else //person *Host = GetPersonFromConfig(Wrap0(Metadata->member)); if(Host) { @@ -6534,6 +7021,8 @@ BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metad } } + // NOTE(matt): As we only cite the speaker when there are a multiple of them, we only need to SortAndAbbreviateSpeakers() + // in the same situation if(Speakers->Speakers.ItemCount > 1) { SortAndAbbreviateSpeakers(Speakers); @@ -6563,9 +7052,10 @@ BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metad " \n" " \n"); } - IndexingError(&HMMLFilepath, 0, S_ERROR, "Missing \"indexer\" in the [video] node", 0); + IndexingError(HMMLFilepath, 0, S_ERROR, "Missing \"indexer\" in the [video] node", 0); return CreditsError_NoIndexer; } +#endif if(CreditsMenu->Ptr > CreditsMenu->Location) { @@ -6978,7 +7468,7 @@ SearchQuotes(memory_book *Strings, buffer *QuoteStaging, int CacheSize, quote_in } else { - while(*QuoteStaging->Ptr != '\n') + while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize && *QuoteStaging->Ptr != '\n') { ++QuoteStaging->Ptr; } @@ -6993,111 +7483,115 @@ BuildQuote(memory_book *Strings, quote_info *Info, string Speaker, int ID, bool { MEM_TEST_TOP("BuildQuote()"); rc Result = RC_SUCCESS; - // TODO(matt): Generally sanitise this function, e.g. using MakeString0(), curling in to a growing buffer, etc. - // NOTE(matt): Stack-string - char QuoteCacheDir[256] = {}; - CopyString(QuoteCacheDir, sizeof(QuoteCacheDir), "%.*s/quotes", (int)Config->CacheDir.Length, Config->CacheDir.Base); - // NOTE(matt): Stack-string - char QuoteCachePath[256] = {}; - CopyString(QuoteCachePath, sizeof(QuoteCachePath), "%s/%.*s", QuoteCacheDir, (int)Speaker.Length, Speaker.Base); - - // NOTE(matt): Stack-string - char QuotesURL[256] = {}; - // TODO(matt): Make the URL configurable and also handle the case in which the .raw isn't available - CopyString(QuotesURL, sizeof(QuotesURL), "https://dev.abaines.me.uk/quotes/%.*s.raw", (int)Speaker.Length, Speaker.Base); - - bool CacheAvailable = FALSE; - FILE *QuoteCache = fopen(QuoteCachePath, "a+"); - if(QuoteCache) + // TODO(matt): Suss out the Speaker more sensibly + if(Speaker.Length > 0) { - CacheAvailable = TRUE; - } - else - { - MakeDir(Wrap0i(QuoteCacheDir, sizeof(QuoteCacheDir))); - QuoteCache = fopen(QuoteCachePath, "a+"); + // TODO(matt): Generally sanitise this function, e.g. using MakeString0(), curling in to a growing buffer, etc. + // NOTE(matt): Stack-string + char QuoteCacheDir[256] = {}; + CopyString(QuoteCacheDir, sizeof(QuoteCacheDir), "%.*s/quotes", (int)Config->CacheDir.Length, Config->CacheDir.Base); + // NOTE(matt): Stack-string + char QuoteCachePath[256] = {}; + CopyString(QuoteCachePath, sizeof(QuoteCachePath), "%s/%.*s", QuoteCacheDir, (int)Speaker.Length, Speaker.Base); + + // NOTE(matt): Stack-string + char QuotesURL[256] = {}; + // TODO(matt): Make the URL configurable and also handle the case in which the .raw isn't available + CopyString(QuotesURL, sizeof(QuotesURL), "https://dev.abaines.me.uk/quotes/%.*s.raw", (int)Speaker.Length, Speaker.Base); + + bool CacheAvailable = FALSE; + FILE *QuoteCache = fopen(QuoteCachePath, "a+"); if(QuoteCache) { CacheAvailable = TRUE; } else { - // TODO(matt): SystemError(); - fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno)); + MakeDir(Wrap0i(QuoteCacheDir, sizeof(QuoteCacheDir))); + QuoteCache = fopen(QuoteCachePath, "a+"); + if(QuoteCache) + { + CacheAvailable = TRUE; + } + else + { + // TODO(matt): SystemError(); + fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno)); + } } - } - buffer QuoteStaging = {}; - QuoteStaging.ID = BID_QUOTE_STAGING; - QuoteStaging.Size = Kilobytes(256); - QuoteStaging.Location = malloc(QuoteStaging.Size); - QuoteStaging.Ptr = QuoteStaging.Location; - int CacheSize = 0; + buffer QuoteStaging = {}; + QuoteStaging.ID = BID_QUOTE_STAGING; + QuoteStaging.Size = Kilobytes(256); + QuoteStaging.Location = malloc(QuoteStaging.Size); + QuoteStaging.Ptr = QuoteStaging.Location; + int CacheSize = 0; - if(QuoteStaging.Location) - { + if(QuoteStaging.Location) + { #if DEBUG_MEM - FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); - fprintf(MemLog, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); - fclose(MemLog); - printf(" Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); + FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); + fprintf(MemLog, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); + fclose(MemLog); + printf(" Allocated QuoteStaging (%ld)\n", QuoteStaging.Size); #endif - if(!ShouldFetchQuotes) - { - if(CacheAvailable) + if(!ShouldFetchQuotes) { - fseek(QuoteCache, 0, SEEK_END); - CacheSize = ftell(QuoteCache); - fseek(QuoteCache, 0, SEEK_SET); + if(CacheAvailable) + { + fseek(QuoteCache, 0, SEEK_END); + CacheSize = ftell(QuoteCache); + fseek(QuoteCache, 0, SEEK_SET); - fread(QuoteStaging.Location, CacheSize, 1, QuoteCache); - fclose(QuoteCache); - rc SearchQuotesResult = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID); - if(SearchQuotesResult == RC_UNFOUND) + fread(QuoteStaging.Location, CacheSize, 1, QuoteCache); + fclose(QuoteCache); + rc SearchQuotesResult = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID); + if(SearchQuotesResult == RC_UNFOUND) + { + ShouldFetchQuotes = TRUE; + } + } + else { ShouldFetchQuotes = TRUE; } } - else - { - ShouldFetchQuotes = TRUE; - } - } - if(ShouldFetchQuotes) - { - /* */ MEM_TEST_MID("BuildQuote()"); - /* +MEM */ CURLcode CurlQuotesResult = CurlQuotes(&QuoteStaging, QuotesURL); - /* */ MEM_TEST_MID("BuildQuote()"); - if(CurlQuotesResult == CURLE_OK) + if(ShouldFetchQuotes) { - LastQuoteFetch = time(0); - CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; - Result = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID); - } - else - { - Result = RC_UNFOUND; - } + /* */ MEM_TEST_MID("BuildQuote()"); + /* +MEM */ CURLcode CurlQuotesResult = CurlQuotes(&QuoteStaging, QuotesURL); + /* */ MEM_TEST_MID("BuildQuote()"); + if(CurlQuotesResult == CURLE_OK) + { + LastQuoteFetch = time(0); + CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; + Result = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID); + } + else + { + Result = RC_UNFOUND; + } - if(CacheAvailable) - { - QuoteCache = fopen(QuoteCachePath, "w"); - fwrite(QuoteStaging.Location, CacheSize, 1, QuoteCache); - fclose(QuoteCache); + if(CacheAvailable) + { + QuoteCache = fopen(QuoteCachePath, "w"); + fwrite(QuoteStaging.Location, CacheSize, 1, QuoteCache); + fclose(QuoteCache); + } } + FreeBuffer(&QuoteStaging); + } + else + { + Result = RC_ERROR_MEMORY; + if(CacheAvailable) { fclose(QuoteCache); } } - FreeBuffer(&QuoteStaging); - } - else - { - Result = RC_ERROR_MEMORY; - if(CacheAvailable) { fclose(QuoteCache); } - } - MEM_TEST_END("BuildQuote()"); + MEM_TEST_END("BuildQuote()"); + } return Result; } @@ -7226,6 +7720,8 @@ ResetConfigIdentifierDescriptionDisplayedBools(void) ConfigIdentifiers[i].IdentifierDescriptionDisplayed = FALSE; ConfigIdentifiers[i].IdentifierDescription_MediumDisplayed = FALSE; ConfigIdentifiers[i].LocalVariableDescriptionDisplayed = FALSE; + ConfigIdentifiers[i].IdentifierDescription_RoleDisplayed = FALSE; + ConfigIdentifiers[i].IdentifierDescription_CreditDisplayed = FALSE; } } @@ -7456,8 +7952,8 @@ PrintHelp_(char *BinaryLocation) " This is equal to the \"project\" field in the HMML files by default\n" "\n" " Project Input Paths\n" - " -d \n" - " Override default annotations directory (\"%s\")\n" + " -d \n" + " Override default timestamps directory (\"%s\")\n" " -t \n" " Override default templates directory (\"%s\")\n" "\n" @@ -8214,7 +8710,7 @@ MediumExists(string HMMLFilepath, string Medium) .Separator = "•", }; - IndexingError(&HMMLFilepath, 0, S_ERROR, "Specified default medium not available: ", &Medium); + IndexingError(HMMLFilepath, 0, S_ERROR, "Specified default medium not available: ", &Medium); fprintf(stderr, "Valid media are:\n"); for(int i = 0; i < CurrentProject->Medium.ItemCount; ++i) { @@ -8809,35 +9305,18 @@ VideoIsPrivate(string VODPlatform, char *VideoID) } } -speaker * -GetSpeaker(memory_book *Speakers, string Username) -{ - speaker *Result = 0; - for(int i = 0; i < Speakers->ItemCount; ++i) - { - speaker *This = GetPlaceInBook(Speakers, i); - if(!StringsDifferCaseInsensitive(This->Person->ID, Username)) - { - Result = This; - break; - } - } - return Result; -} - -void -FreeSpeakers(speakers *Speakers) -{ - FreeBook(&Speakers->Speakers); - FreeBook(&Speakers->Abbreviations); -} - bool -IsCategorisedAFK(HMML_Annotation *Anno) +IsCategorisedAFK( +#if(HMMLIB_MAJOR_VERSION == 2) + HMML_Timestamp +#else + HMML_Annotation +#endif + *Timestamp) { - for(int i = 0; i < Anno->marker_count; ++i) + for(int i = 0; i < Timestamp->marker_count; ++i) { - if(!StringsDiffer0(Anno->markers[i].marker, "afk")) + if(!StringsDiffer0(Timestamp->markers[i].marker, "afk")) { return TRUE; } @@ -8846,11 +9325,17 @@ IsCategorisedAFK(HMML_Annotation *Anno) } bool -IsCategorisedAuthored(HMML_Annotation *Anno) +IsCategorisedAuthored( +#if(HMMLIB_MAJOR_VERSION == 2) + HMML_Timestamp +#else + HMML_Annotation +#endif + *Timestamp) { - for(int i = 0; i < Anno->marker_count; ++i) + for(int i = 0; i < Timestamp->marker_count; ++i) { - if(!StringsDiffer0(Anno->markers[i].marker, "authored")) + if(!StringsDiffer0(Timestamp->markers[i].marker, "authored")) { return TRUE; } @@ -9250,23 +9735,62 @@ FreeReferences(_memory_book(ref_info) *References) FreeBook(References); } +bool +HMMLBaseFilenameIs(neighbourhood *N, char *ID) +{ + return StringsMatch(Wrap0i(N->WorkingThis.HMMLBaseFilename, sizeof(N->WorkingThis.HMMLBaseFilename)), Wrap0(ID)); +} + +bool +HMMLOutputLocationIs(neighbourhood *N, char *OutputLocation) +{ + return StringsMatch(Wrap0i(N->WorkingThis.OutputLocation, sizeof(N->WorkingThis.OutputLocation)), Wrap0(OutputLocation)); +} + +bool +TimecodeIs(v3 Timecode, int Hours, int Minutes, int Seconds) +{ + return Timecode.Hours == Hours && Timecode.Minutes == Minutes && Timecode.Seconds == Seconds; +} + rc -ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, memory_book *Strings, +ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, memory_book *Strings, menu_buffers *MenuBuffers, index_buffers *IndexBuffers, player_buffers *PlayerBuffers, - medium *DefaultMedium, speakers *Speakers, person *Host, string Author, + medium *DefaultMedium, speakers *Speakers, +#if(HMMLIB_MAJOR_VERSION == 2) +#else + person *Host, +#endif + string Author, _memory_book(ref_info) *ReferencesArray, bool *HasQuoteMenu, bool *HasReferenceMenu, bool *HasFilterMenu, bool *RequiresCineraJS, int *QuoteIdentifier, int *RefIdentifier, _memory_book(category_info) *Topics, _memory_book(category_info) *Media, - HMML_Annotation *Anno, char **PreviousTimestamp) +#if(HMMLIB_MAJOR_VERSION == 2) + HMML_Timestamp *Timestamp, + v3 *PreviousTimecode +#else + HMML_Annotation *Timestamp, + char **PreviousTimecode +#endif + ) { - MEM_TEST_TOP("ProcessTimecode"); + MEM_TEST_TOP("ProcessTimestamp"); // TODO(matt): Introduce and use a SystemError() in here rc Result = RC_SUCCESS; - if(!*PreviousTimestamp || TimecodeToSeconds(Anno->time) >= TimecodeToSeconds(*PreviousTimestamp)) +#if(HMMLIB_MAJOR_VERSION == 2) + v3 Timecode = V3(Timestamp->h, Timestamp->m, Timestamp->s); + if(TimecodeToSeconds(Timecode) >= TimecodeToSeconds(*PreviousTimecode)) +#else + if(!*PreviousTimecode || TimecodeToSeconds(Timestamp->time) >= TimecodeToSeconds(*PreviousTimecode)) +#endif { - *PreviousTimestamp = Anno->time; +#if(HMMLIB_MAJOR_VERSION == 2) + *PreviousTimecode = Timecode; +#else + *PreviousTimecode = Timestamp->time; +#endif memory_book LocalTopics = InitBook(MBT_CATEGORY_INFO, 8); memory_book LocalMedia = InitBook(MBT_CATEGORY_INFO, 8); @@ -9283,17 +9807,29 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me RewindBuffer(&IndexBuffers->Text); RewindBuffer(&IndexBuffers->CategoryIcons); + +#if(HMMLIB_MAJOR_VERSION == 2) CopyStringToBuffer(&IndexBuffers->Header, "
time)); + TimecodeToSeconds(Timecode)); +#else + CopyStringToBuffer(&IndexBuffers->Header, + "
time)); +#endif CopyStringToBuffer(&IndexBuffers->Class, " class=\"marker"); - speaker *Speaker = GetSpeaker(&Speakers->Speakers, Anno->author ? Wrap0(Anno->author) : CurrentProject->StreamUsername.Length > 0 ? CurrentProject->StreamUsername : Host->ID); - if(!IsCategorisedAFK(Anno)) +#if(HMMLIB_MAJOR_VERSION == 2) + speaker *Speaker = GetSpeaker(&Speakers->Speakers, Author); +#else + speaker *Speaker = GetSpeaker(&Speakers->Speakers, Timestamp->author ? Wrap0(Timestamp->author) : CurrentProject->StreamUsername.Length > 0 ? CurrentProject->StreamUsername : Host->ID); +#endif + if(!IsCategorisedAFK(Timestamp)) { - if(Speakers->Speakers.ItemCount > 1 && Speaker && !IsCategorisedAuthored(Anno)) + // NOTE(matt): I reckon it's fair to only cite the speaker when there are a multiple of them + if(Speakers->Speakers.ItemCount > 1 && Speaker && !IsCategorisedAuthored(Timestamp)) { string DisplayName = !Speaker->Seen ? Speaker->Person->Name : Speaker->Abbreviation; @@ -9306,7 +9842,7 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me Speaker->Seen = TRUE; } - else if(Anno->author) + else if(Author.Length > 0) { if(!*HasFilterMenu) { @@ -9314,37 +9850,37 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me } InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored")); hsl_colour AuthorColour; - StringToColourHash(&AuthorColour, Wrap0(Anno->author)); + StringToColourHash(&AuthorColour, Author); // TODO(matt): That EDITION_NETWORK site database API-polling stuff CopyStringToBuffer(&IndexBuffers->Text, - "%s ", + "%.*s ", AuthorColour.Hue, AuthorColour.Saturation, - Anno->author); + (int)Author.Length, Author.Base); } } - char *InPtr = Anno->text; + char *InPtr = Timestamp->text; int MarkerIndex = 0, RefIndex = 0; - while(*InPtr || RefIndex < Anno->reference_count) + while(*InPtr || RefIndex < Timestamp->reference_count) { - if(MarkerIndex < Anno->marker_count && - InPtr - Anno->text == Anno->markers[MarkerIndex].offset) + if(MarkerIndex < Timestamp->marker_count && + InPtr - Timestamp->text == Timestamp->markers[MarkerIndex].offset) { - char *Readable = Anno->markers[MarkerIndex].parameter - ? Anno->markers[MarkerIndex].parameter - : Anno->markers[MarkerIndex].marker; - HMML_MarkerType Type = Anno->markers[MarkerIndex].type; + char *Readable = Timestamp->markers[MarkerIndex].parameter + ? Timestamp->markers[MarkerIndex].parameter + : Timestamp->markers[MarkerIndex].marker; + HMML_MarkerType Type = Timestamp->markers[MarkerIndex].type; if(Type == HMML_CATEGORY) { - Result = GenerateTopicColours(N, Wrap0(Anno->markers[MarkerIndex].marker)); + Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker)); if(Result == RC_SUCCESS) { if(!*HasFilterMenu) { *HasFilterMenu = TRUE; } - InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Anno->markers[MarkerIndex].marker)); + InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker)); CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr); } else @@ -9356,10 +9892,10 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me { // TODO(matt): That EDITION_NETWORK site database API-polling stuff hsl_colour Colour; - StringToColourHash(&Colour, Wrap0(Anno->markers[MarkerIndex].marker)); + StringToColourHash(&Colour, Wrap0(Timestamp->markers[MarkerIndex].marker)); CopyStringToBuffer(&IndexBuffers->Text, "%.*s", - Anno->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project", + Timestamp->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project", Colour.Hue, Colour.Saturation, (int)StringLength(Readable), InPtr); } @@ -9370,10 +9906,10 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me if(Result == RC_SUCCESS) { - while(RefIndex < Anno->reference_count && - InPtr - Anno->text == Anno->references[RefIndex].offset) + while(RefIndex < Timestamp->reference_count && + InPtr - Timestamp->text == Timestamp->references[RefIndex].offset) { - HMML_Reference *CurrentRef = Anno->references + RefIndex; + HMML_Reference *CurrentRef = Timestamp->references + RefIndex; if(!*HasReferenceMenu) { CopyStringToBuffer(&MenuBuffers->Reference, @@ -9388,7 +9924,11 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me if(This) { identifier *New = MakeSpaceInBook(&This->Identifier); - New->Timecode = Anno->time; +#if(HMMLIB_MAJOR_VERSION == 2) + New->Timecode = Timecode; +#else + New->Timecode = Timestamp->time; +#endif New->Identifier = *RefIdentifier; } else @@ -9397,13 +9937,13 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me { case RC_INVALID_REFERENCE: { - IndexingError(&Filepath, Anno->line, S_ERROR, + IndexingError(Filepath, Timestamp->line, S_ERROR, "Invalid [ref]. Please set either a \"url\" or \"isbn\"", 0); } break; case RC_UNHANDLED_REF_COMBO: { - IndexingError(&Filepath, Anno->line, S_ERROR, + IndexingError(Filepath, Timestamp->line, S_ERROR, "Cannot process new combination of reference info\n" "\n" "Either tweak your timestamp, or contact miblodelcarpio@gmail.com\n" @@ -9418,9 +9958,8 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !HasReference ? " data-ref=\"" : "," , This->ID); HasReference = TRUE; - CopyStringToBuffer(&IndexBuffers->Text, "%s%d", - Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset ? "," : "", + RefIndex > 0 && Timestamp->references[RefIndex].offset == Timestamp->references[RefIndex-1].offset ? "," : "", *RefIdentifier); ++RefIndex; @@ -9463,7 +10002,11 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me if(Result == RC_SUCCESS) { - if(Anno->is_quote) +#if(HMMLIB_MAJOR_VERSION == 2) + if(Timestamp->quote.present) +#else + if(Timestamp->is_quote) +#endif { if(!*HasQuoteMenu) { @@ -9492,9 +10035,32 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me ShouldFetchQuotes = TRUE; } - /* */ MEM_TEST_MID("ProcessTimecode()"); - /* +MEM */ Result = BuildQuote(Strings, &QuoteInfo, Author, Anno->quote.id, ShouldFetchQuotes); - /* */ MEM_TEST_MID("ProcessTimecode()"); + if(!Speaker && Speakers->Speakers.ItemCount > 0) + { + Speaker = GetPlaceInBook(&Speakers->Speakers, 0); + } + string QuoteUsername; +#if(HMMLIB_MAJOR_VERSION == 2) + if(Timestamp->quote.author) + { + QuoteUsername = Wrap0(Timestamp->quote.author); + } + else if(Speaker) + { + QuoteUsername = Speaker->Person->QuoteUsername; + } + else + { + QuoteUsername = Author; + } +#else + QuoteUsername = Author; +#endif + + /* */ MEM_TEST_MID("ProcessTimestamp()"); + /* +MEM */ Result = BuildQuote(Strings, &QuoteInfo, + QuoteUsername, Timestamp->quote.id, ShouldFetchQuotes); + /* */ MEM_TEST_MID("ProcessTimestamp()"); if(Result == RC_SUCCESS) { CopyStringToBuffer(&MenuBuffers->Quote, @@ -9503,13 +10069,30 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me " \n" "
Quote %d
\n" "
", - (int)Author.Length, Author.Base, - Anno->quote.id, + (int)QuoteUsername.Length, QuoteUsername.Base, + Timestamp->quote.id, *QuoteIdentifier, - Anno->quote.id); + Timestamp->quote.id); CopyStringToBufferHTMLSafe(&MenuBuffers->Quote, QuoteInfo.Text); +#if(HMMLIB_MAJOR_VERSION == 2) + string DateString = UnixTimeToDateString(Strings, QuoteInfo.Date); + CopyStringToBuffer(&MenuBuffers->Quote, "
\n" + " \n" + "
\n" + "
\n" + " [&#%d;]", + (int)QuoteUsername.Length, QuoteUsername.Base, + (int)DateString.Length, DateString.Base, // TODO(matt): Convert Unixtime to date-string + TimecodeToSeconds(Timecode), + *QuoteIdentifier); + CopyTimecodeToBuffer(&MenuBuffers->Quote, Timecode); + CopyStringToBuffer(&MenuBuffers->Quote, "\n" + "
\n" + " \n" + " \n"); +#else string DateString = UnixTimeToDateString(Strings, QuoteInfo.Date); CopyStringToBuffer(&MenuBuffers->Quote, "
\n" " \n" @@ -9519,12 +10102,13 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me "
\n" " \n" " \n", - (int)Author.Length, Author.Base, + (int)QuoteUsername.Length, QuoteUsername.Base, (int)DateString.Length, DateString.Base, // TODO(matt): Convert Unixtime to date-string - TimecodeToSeconds(Anno->time), + TimecodeToSeconds(Timestamp->time), *QuoteIdentifier, - Anno->time); - if(!Anno->text[0]) + Timestamp->time); +#endif + if(!Timestamp->text[0]) { CopyStringToBuffer(&IndexBuffers->Text, "“"); CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text); @@ -9535,14 +10119,24 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me } else if(Result == RC_UNFOUND) { - IndexingQuoteError(&Filepath, Anno->line, Author, Anno->quote.id); + IndexingQuoteError(&Filepath, Timestamp->line, QuoteUsername, Timestamp->quote.id); } } if(Result == RC_SUCCESS) { - CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%d\": \"", TimecodeToSeconds(Anno->time)); - if(Anno->is_quote && !Anno->text[0]) +#if(HMMLIB_MAJOR_VERSION == 2) + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%d\": \"", TimecodeToSeconds(Timecode)); +#else + CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%d\": \"", TimecodeToSeconds(Timestamp->time)); +#endif + if( +#if(HMMLIB_MAJOR_VERSION == 2) + Timestamp->quote.present && +#else + Timestamp->is_quote && +#endif + !Timestamp->text[0]) { CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C"); CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, QuoteInfo.Text); @@ -9550,20 +10144,20 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me } else { - CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(Anno->text)); + CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(Timestamp->text)); } CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"); - while(MarkerIndex < Anno->marker_count) + while(MarkerIndex < Timestamp->marker_count) { - Result = GenerateTopicColours(N, Wrap0(Anno->markers[MarkerIndex].marker)); + Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker)); if(Result == RC_SUCCESS) { if(!*HasFilterMenu) { *HasFilterMenu = TRUE; } - InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Anno->markers[MarkerIndex].marker)); + InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker)); ++MarkerIndex; } else @@ -9601,9 +10195,16 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me CopyStringToBuffer(&IndexBuffers->Header, ">\n"); CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Header, 0, PAGE_PLAYER); +#if(HMMLIB_MAJOR_VERSION == 2) + CopyStringToBuffer(&IndexBuffers->Master, + "
"); + CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); + CopyStringToBuffer(&IndexBuffers->Master, ""); +#else CopyStringToBuffer(&IndexBuffers->Master, "
%s", - Anno->time); + Timestamp->time); +#endif CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); @@ -9612,10 +10213,18 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS); } +#if(HMMLIB_MAJOR_VERSION == 2) + CopyStringToBuffer(&IndexBuffers->Master, "
\n" + "
\n" + "
"); + CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); + CopyStringToBuffer(&IndexBuffers->Master, ""); +#else CopyStringToBuffer(&IndexBuffers->Master, "
\n" "
\n" "
%s", - Anno->time); + Timestamp->time); +#endif CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); @@ -9624,11 +10233,20 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS); } +#if(HMMLIB_MAJOR_VERSION == 2) + CopyStringToBuffer(&IndexBuffers->Master, "
\n" + "
\n" + "
\n" + "
"); + CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); + CopyStringToBuffer(&IndexBuffers->Master, ""); +#else CopyStringToBuffer(&IndexBuffers->Master, "
\n" "
\n" "
\n" "
%s", - Anno->time); + Timestamp->time); +#endif CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); @@ -9653,10 +10271,14 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me } else { - IndexingChronologyError(&Filepath, Anno->line, Anno->time, *PreviousTimestamp); +#if(HMMLIB_MAJOR_VERSION == 2) + IndexingChronologyError(&Filepath, Timestamp->line, Timecode, *PreviousTimecode); +#else + IndexingChronologyError(&Filepath, Timestamp->line, Timestamp->time, *PreviousTimecode); +#endif Result = RC_ERROR_HMML; } - MEM_TEST_END("ProcessTimecode"); + MEM_TEST_END("ProcessTimestamp"); return Result; } @@ -9722,7 +10344,13 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF if(InFile) { +#if(HMMLIB_MAJOR_VERSION == 2) + char *HMMLContents = ReadFileIntoMemory0(InFile); + HMML_Output HMML = hmml_parse(HMMLContents); + Free(HMMLContents); // TODO(matt): Maybe we'll need to free this later? +#else HMML_Output HMML = hmml_parse_file(InFile); +#endif fclose(InFile); if(HMML.well_formed) @@ -9746,7 +10374,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF if(!HMML.metadata.title) { - IndexingError(&FilepathL, 0, S_ERROR, "The [video] node lacks a \"title\"", 0); + IndexingError(FilepathL, 0, S_ERROR, "The [video] node lacks a \"title\"", 0); Result = RC_ERROR_HMML; } else if(StringLength(HMML.metadata.title) > MAX_TITLE_LENGTH) @@ -9760,29 +10388,33 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF Title = Wrap0(HMML.metadata.title); } +#if(HMMLIB_MAJOR_VERSION == 2) +#else person *Host = CurrentProject->Owner; - if(HMML.metadata.member) +#endif + +#if(HMMLIB_MAJOR_VERSION == 2) + for(int CreditIndex = 0; CreditIndex < HMML.metadata.credit_count; ++CreditIndex) { - Host = GetPersonFromConfig(Wrap0(HMML.metadata.member)); - if(!Host && CurrentProject->Owner) + HMML_Credit *This = HMML.metadata.credits + CreditIndex; + role *Role = GetRoleByID(Config, Wrap0(This->role)); + if(!Role) { - fprintf(stderr, "Falling back to the owner set in the config: "); - PrintStringCN(CS_MAGENTA_BOLD, CurrentProject->Owner->ID, FALSE, TRUE); - Host = CurrentProject->Owner; + ErrorRole(FilepathL, Wrap0(This->role)); + Result = CreditsError_NoRole; // TODO(matt): Is this a fine error here? + } + person *Person = GetPersonFromConfig(Wrap0(This->name)); + if(!Person) + { + ErrorCredentials(FilepathL, Wrap0(This->name), Role); + Result = CreditsError_NoCredentials; // TODO(matt): Is this a fine error here? } } - - if(!Host) - { - IndexingError(&FilepathL, 0, S_ERROR, "No known owner set in the config, or member in the [video] node", 0); - Result = RC_ERROR_HMML; - //ErrorCredentials(FilepathL, Wrap0(HMML.metadata.member), R_HOST); - //Result = RC_ERROR_HMML; - } +#endif if(!HMML.metadata.id) { - IndexingError(&FilepathL, 0, S_ERROR, "The [video] node lacks an \"id\"", 0); + IndexingError(FilepathL, 0, S_ERROR, "The [video] node lacks an \"id\"", 0); Result = RC_ERROR_HMML; } else @@ -9802,7 +10434,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF if(VODPlatform.Length == 0) { - IndexingError(&FilepathL, 0, S_ERROR, "The [video] node lacks an \"vod_platform\"", 0); + IndexingError(FilepathL, 0, S_ERROR, "The [video] node lacks an \"vod_platform\"", 0); Result = RC_ERROR_HMML; } else @@ -9830,7 +10462,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF if(!DefaultMedium) { - IndexingError(&FilepathL, 0, S_ERROR, "No default_medium set in config, or available medium set in the [video] node", 0); + IndexingError(FilepathL, 0, S_ERROR, "No default_medium set in config, or available medium set in the [video] node", 0); Result = RC_ERROR_HMML; } @@ -9849,6 +10481,44 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF // TODO(matt): Consider simply making these as buffers and claiming the necessary amount for them // The nice thing about doing it this way, though, is that it encourages bespoke template use, which should // usually be the more convenient way for people to write greater amounts of localised information + +#if(HMMLIB_MAJOR_VERSION == 2) + for(int CustomIndex = 0; CustomIndex < HMML.metadata.custom_count; ++CustomIndex) + { + HMML_VideoCustomMetaData *This = HMML.metadata.custom + CustomIndex; + misc_attribute *ThisAttribute = GetMiscAttribute(This); + if(ThisAttribute && ThisAttribute->Type == MAT_CUSTOM) + { + if(StringLength(This->value) > (ThisAttribute->CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH)) + { + IndexingErrorCustomSizing(&FilepathL, 0, ThisAttribute->CustomIndex, Wrap0(This->value)); + Result = RC_ERROR_HMML; + } + else + { + switch(ThisAttribute->CustomIndex) + { + case 0: CopyStringNoFormat(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0), Wrap0(This->value)); break; + case 1: CopyStringNoFormat(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1), Wrap0(This->value)); break; + case 2: CopyStringNoFormat(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2), Wrap0(This->value)); break; + case 3: CopyStringNoFormat(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3), Wrap0(This->value)); break; + case 4: CopyStringNoFormat(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4), Wrap0(This->value)); break; + case 5: CopyStringNoFormat(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5), Wrap0(This->value)); break; + case 6: CopyStringNoFormat(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6), Wrap0(This->value)); break; + case 7: CopyStringNoFormat(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7), Wrap0(This->value)); break; + case 8: CopyStringNoFormat(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8), Wrap0(This->value)); break; + case 9: CopyStringNoFormat(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9), Wrap0(This->value)); break; + case 10: CopyStringNoFormat(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10), Wrap0(This->value)); break; + case 11: CopyStringNoFormat(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11), Wrap0(This->value)); break; + case 12: CopyStringNoFormat(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12), Wrap0(This->value)); break; + case 13: CopyStringNoFormat(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13), Wrap0(This->value)); break; + case 14: CopyStringNoFormat(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14), Wrap0(This->value)); break; + case 15: CopyStringNoFormat(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15), Wrap0(This->value)); break; + } + } + } + } +#else for(int CustomIndex = 0; CustomIndex < HMML_CUSTOM_ATTR_COUNT; ++CustomIndex) { if(HMML.metadata.custom[CustomIndex]) @@ -9882,6 +10552,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF } } } +#endif if(Result == RC_SUCCESS && !CurrentProject->DenyBespokeTemplates && HMML.metadata.template) { @@ -9995,7 +10666,12 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF "
\n"); bool RequiresCineraJS = FALSE; - Result = BuildCredits(FilepathL, &MenuBuffers.Credits, &HMML.metadata, Host, &Speakers, &RequiresCineraJS); + Result = BuildCredits(FilepathL, &MenuBuffers.Credits, &HMML.metadata, +#if(HMMLIB_MAJOR_VERSION == 2) +#else + Host, +#endif + &Speakers, &RequiresCineraJS); if(Result == RC_SUCCESS) { CopyStringToBuffer(&CollationBuffers->SearchEntry, "location: \"%.*s\"\n", (int)OutputLocation.Length, OutputLocation.Base); @@ -10011,9 +10687,19 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF printf("\n\n --- Entering Timestamps Loop ---\n\n\n\n"); #endif +#if(HMMLIB_MAJOR_VERSION == 2) + v3 PreviousTimecode = {}; +#else char *PreviousTimecode = 0; +#endif + for(int TimestampIndex = 0; TimestampIndex < - for(int TimestampIndex = 0; TimestampIndex < HMML.annotation_count; ++TimestampIndex) +#if(HMMLIB_MAJOR_VERSION == 2) + HMML.timestamp_count +#else + HMML.annotation_count +#endif + ; ++TimestampIndex) { // TODO(matt): Thoroughly test this reorganisation // @@ -10025,22 +10711,35 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF printf("%d\n", TimestampIndex); #endif - HMML_Annotation *Anno = HMML.annotations + TimestampIndex; +#if(HMMLIB_MAJOR_VERSION == 2) + HMML_Timestamp *Timestamp = HMML.timestamps + TimestampIndex; +#else + HMML_Annotation *Timestamp = HMML.annotations + TimestampIndex; +#endif string Author = {}; - if(Anno->quote.author) { Author = Wrap0(Anno->quote.author); } +#if(HMMLIB_MAJOR_VERSION == 2) + if(Timestamp->author) { Author = Wrap0(Timestamp->author); } +#else + if(Timestamp->quote.author) { Author = Wrap0(Timestamp->quote.author); } else if(HMML.metadata.stream_username) { Author = Wrap0(HMML.metadata.stream_username); } else if(CurrentProject->StreamUsername.Length > 0) { Author = CurrentProject->StreamUsername; } else { Author = Host->ID; } +#endif /* */ MEM_TEST_MID("HMMLToBuffers"); - /* +MEM */ Result = ProcessTimecode(CollationBuffers, N, Wrap0(Filepath), &Strings, + /* +MEM */ Result = ProcessTimestamp(CollationBuffers, N, Wrap0(Filepath), &Strings, /* */ &MenuBuffers, &IndexBuffers, &PlayerBuffers, - /* */ DefaultMedium, &Speakers, Host, Author, &ReferencesArray, + /* */ DefaultMedium, &Speakers, +#if(HMMLIB_MAJOR_VERSION == 2) +#else + Host, +#endif + Author, &ReferencesArray, /* */ &HasQuoteMenu, &HasReferenceMenu, &HasFilterMenu, &RequiresCineraJS, /* */ &QuoteIdentifier, &RefIdentifier, /* */ &Topics, &Media, - /* */ Anno, &PreviousTimecode); + /* */ Timestamp, &PreviousTimecode); /* */ MEM_TEST_MID("HMMLToBuffers"); if(Result != RC_SUCCESS) { @@ -10100,12 +10799,20 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF "
\n "); for(int k = 0; k < 3 && j < This->Identifier.ItemCount; ++k, ++j) { +#if(HMMLIB_MAJOR_VERSION == 2) + identifier *ThisIdentifier = GetPlaceInBook(&This->Identifier, j); + CopyStringToBuffer(&MenuBuffers.Reference, + "[%d]", TimecodeToSeconds(ThisIdentifier->Timecode), ThisIdentifier->Identifier); + CopyTimecodeToBuffer(&MenuBuffers.Reference, ThisIdentifier->Timecode); + CopyStringToBuffer(&MenuBuffers.Reference, ""); +#else identifier *ThisIdentifier = GetPlaceInBook(&This->Identifier, j); CopyStringToBuffer(&MenuBuffers.Reference, "[%d]%s", TimecodeToSeconds(ThisIdentifier->Timecode), ThisIdentifier->Identifier, ThisIdentifier->Timecode); +#endif } CopyStringToBuffer(&MenuBuffers.Reference, "\n" "
\n"); @@ -10596,7 +11303,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF else { LogError(LOG_ERROR, "%s:%d: %s", Filepath, HMML.error.line, HMML.error.message); - IndexingError(&FilepathL, HMML.error.line, S_ERROR, HMML.error.message, 0); + IndexingError(FilepathL, HMML.error.line, S_ERROR, HMML.error.message, 0); PrintEdit(EDIT_SKIP, CurrentProject->Lineage, BaseFilename, 0, FALSE, TRUE); Result = RC_ERROR_HMML; } @@ -10886,7 +11593,7 @@ SortLandmarks(memory_book *A) } rc -BuffersToHTML(config *C, project *Project, buffers *CollationBuffers, template *Template, char *OutputPath, page_type PageType, unsigned int *PlayerOffset) +BuffersToHTML(config *C, project *Project, buffers *CollationBuffers, template *Template, char *OutputPath, page_type PageType, bool GlobalSearch, unsigned int *PlayerOffset) { rc Result = RC_SUCCESS; MEM_TEST_TOP("BuffersToHTML()"); @@ -10969,7 +11676,14 @@ BuffersToHTML(config *C, project *Project, buffers *CollationBuffers, template * } else { - CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch))); + if(GlobalSearch) + { + CopyStringToBufferNoFormat(&Master, Config->GlobalSearchURL); + } + else + { + CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch))); + } } break; case TAG_VIDEO_ID: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->VideoID, sizeof(CollationBuffers->VideoID))); break; @@ -11807,8 +12521,6 @@ typedef struct void OffsetAssociatedIndex(asset_index_and_location *AssetDeletionRecords, int AssetDeletionRecordCount, int32_t *Index) { - // TODO(matt): Do we really need to print this? - //fprintf(stderr, "Possibly offsetting associated index\n"); for(int i = AssetDeletionRecordCount - 1; i >= 0; --i) { if(*Index > AssetDeletionRecords[i].Index) @@ -13655,7 +14367,7 @@ GeneratePlayerPage(neighbourhood *N, buffers *CollationBuffers, template *Player } } /* */ MEM_TEST_MID("GeneratePlayerPage()"); - /* +MEM */ BuffersToHTML(0, CurrentProject, CollationBuffers, PlayerTemplate, PlayerPath, PAGE_PLAYER, &N->This->LinkOffsets.PrevStart); + /* +MEM */ BuffersToHTML(0, CurrentProject, CollationBuffers, PlayerTemplate, PlayerPath, PAGE_PLAYER, FALSE, &N->This->LinkOffsets.PrevStart); /* */ MEM_TEST_MID("GeneratePlayerPage()"); Free(PlayerPath); // NOTE(matt): A previous InsertNeighbourLink() call will have done SnipeEntryIntoMetadataBuffer(), but we must do it here now @@ -13698,7 +14410,7 @@ GenerateSearchPage(neighbourhood *N, buffers *CollationBuffers, db_header_projec { case RC_SUCCESS: { - BuffersToHTML(0, P, CollationBuffers, &P->SearchTemplate, SearchPath, PAGE_SEARCH, 0); + BuffersToHTML(0, P, CollationBuffers, &P->SearchTemplate, SearchPath, PAGE_SEARCH, FALSE, 0); UpdateLandmarksForSearch(N, P->Index); break; } @@ -13743,7 +14455,7 @@ GenerateGlobalSearchPage(neighbourhood *N, buffers *CollationBuffers) case RC_SUCCESS: { /* */ MEM_TEST_MID("GenerateGlobalSearchPage()"); - /* +MEM */ BuffersToHTML(Config, 0, CollationBuffers, &Config->SearchTemplate, SearchPath, PAGE_SEARCH, 0); + /* +MEM */ BuffersToHTML(Config, 0, CollationBuffers, &Config->SearchTemplate, SearchPath, PAGE_SEARCH, TRUE, 0); /* */ MEM_TEST_MID("GenerateGlobalSearchPage()"); UpdateLandmarksForSearch(N, GLOBAL_SEARCH_PAGE_INDEX); break; @@ -13770,7 +14482,6 @@ int GenerateSearchPages(neighbourhood *N, buffers *CollationBuffers) { MEM_TEST_TOP("GenerateSearchPages()"); - // TODO(matt): Ascend through all the ancestry, generating search pages PrintFunctionName("GenerateSearchPage()"); project *This = CurrentProject; //PrintLineage(This->Lineage, TRUE); @@ -15373,23 +16084,6 @@ SyncGlobalPagesWithInput(neighbourhood *N, buffers *CollationBuffers) MEM_TEST_END("SyncGlobalPagesWithInput"); } -void -SetCacheDirectory(config *DefaultConfig) -{ - // TODO(matt): This might not have to exist because we should have a default cache directory already set from the config - int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND; - wordexp_t Expansions = {}; - wordexp("$XDG_CACHE_HOME/cinera", &Expansions, Flags); - wordexp("$HOME/.cache/cinera", &Expansions, Flags); - if(Expansions.we_wordc > 0 ) - { -#if AFE - CopyString(DefaultConfig->CacheDir, sizeof(DefaultConfig->CacheDir), Expansions.we_wordv[0]); -#endif - } - wordfree(&Expansions); -} - void InitMemoryArena(arena *Arena, int Size) { @@ -15617,14 +16311,8 @@ InitAll(neighbourhood *Neighbourhood, buffers *CollationBuffers, template *Bespo if(Result == RC_SUCCESS) { - // TODO(matt): Straight up remove these PrintAssetsBlock() calls - //PrintAssetsBlock(0); - SyncDB(Config); - //PrintAssetsBlock(0); - - printf("\n╾─ Hashing assets ─╼\n"); // NOTE(matt): This had to happen before PackTemplate() because those guys may need to do PushAsset() and we must // ensure that the builtin assets get placed correctly @@ -15654,12 +16342,15 @@ InitAll(neighbourhood *Neighbourhood, buffers *CollationBuffers, template *Bespo MEM_TEST_END("InitAll()"); - fprintf(stderr, - "\n" - "╾─ Monitoring file system for %snew%s, %sedited%s and %sdeleted%s .hmml and asset files ─╼\n", - ColourStrings[EditTypes[EDIT_ADDITION].Colour], ColourStrings[CS_END], - ColourStrings[EditTypes[EDIT_REINSERTION].Colour], ColourStrings[CS_END], - ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END]); + if(GlobalRunning && inotifyInstance != -1) + { + fprintf(stderr, + "\n" + "╾─ Monitoring file system for %snew%s, %sedited%s and %sdeleted%s .hmml and asset files ─╼\n", + ColourStrings[EditTypes[EDIT_ADDITION].Colour], ColourStrings[CS_END], + ColourStrings[EditTypes[EDIT_REINSERTION].Colour], ColourStrings[CS_END], + ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END]); + } } return Result; } @@ -15699,33 +16390,43 @@ GetWatchFileForEvent(struct inotify_event *Event) watch_handle *ThisWatch = GetPlaceInBook(&WatchHandles.Handles, HandleIndex); if(Event->wd == ThisWatch->Descriptor) { - if(Event->mask & IN_DELETE_SELF || Event->mask == (IN_CREATE | IN_ISDIR)) - { - Update = TRUE; - } - if(StringsMatch(ThisWatch->TargetPath, ThisWatch->WatchedPath)) { - for(int FileIndex = 0; FileIndex < ThisWatch->Files.ItemCount; ++FileIndex) + if(Event->mask & IN_DELETE_SELF) { - watch_file *ThisFile = GetPlaceInBook(&ThisWatch->Files, FileIndex); - if(ThisFile->Extension != EXT_NULL) + Update = TRUE; + } + else + { + for(int FileIndex = 0; FileIndex < ThisWatch->Files.ItemCount; ++FileIndex) { - if(ExtensionMatches(Wrap0(Event->name), ThisFile->Extension)) + watch_file *ThisFile = GetPlaceInBook(&ThisWatch->Files, FileIndex); + if(ThisFile->Extension != EXT_NULL) + { + if(ExtensionMatches(Wrap0(Event->name), ThisFile->Extension)) + { + Result = ThisFile; + break; + } + } + else if(StringsMatch(Wrap0(Event->name), ThisFile->Path)) { Result = ThisFile; break; } } - else if(StringsMatch(Wrap0(Event->name), ThisFile->Path)) + if(Result) { - Result = ThisFile; break; } } - if(Result) + } + else + { + string WatchablePath = GetNearestExistingPath(ThisWatch->TargetPath); + if(StringsDiffer(WatchablePath, ThisWatch->WatchedPath)) { - break; + Update = TRUE; } } } @@ -15734,6 +16435,7 @@ GetWatchFileForEvent(struct inotify_event *Event) if(Update) { UpdateWatchHandles(Event->wd); + Result = GetWatchFileForEvent(Event); } return Result; } @@ -15942,7 +16644,10 @@ void Exit(void) { Free(MemoryArena.Location); - fprintf(stderr, "Exiting\n"); + if(!GlobalRunning) + { + PrintC(CS_SUCCESS, "\nExiting cleanly. Thank you for indexing with Cinera\n"); + } _exit(0); } @@ -15970,6 +16675,7 @@ InitMemoryBookTypeWidths(void) case MBT_CONFIG_STRING_ASSOCIATION: MemoryBookTypeWidths[i] = sizeof(config_string_association); break; case MBT_CONFIG_TYPE_FIELD: MemoryBookTypeWidths[i] = sizeof(config_type_field); break; case MBT_CONFIG_TYPE_SPEC: MemoryBookTypeWidths[i] = sizeof(config_type_spec); break; + case MBT_CREDIT: MemoryBookTypeWidths[i] = sizeof(credit); break; case MBT_IDENTIFIER: MemoryBookTypeWidths[i] = sizeof(identifier); break; case MBT_LANDMARK: MemoryBookTypeWidths[i] = sizeof(landmark); break; case MBT_MEDIUM: MemoryBookTypeWidths[i] = sizeof(medium); break; @@ -15978,6 +16684,7 @@ InitMemoryBookTypeWidths(void) case MBT_PROJECT: MemoryBookTypeWidths[i] = sizeof(project); break; case MBT_REF_INFO: MemoryBookTypeWidths[i] = sizeof(ref_info); break; case MBT_RESOLUTION_ERROR: MemoryBookTypeWidths[i] = sizeof(resolution_error); break; + case MBT_ROLE: MemoryBookTypeWidths[i] = sizeof(role); break; case MBT_SCOPE_TREE: MemoryBookTypeWidths[i] = sizeof(scope_tree); break; case MBT_SPEAKER: MemoryBookTypeWidths[i] = sizeof(speaker); break; case MBT_SUPPORT: MemoryBookTypeWidths[i] = sizeof(support); break; @@ -15993,6 +16700,7 @@ InitMemoryBookTypeWidths(void) case MBT_NONE: case MBT_PERSON_PTR: case MBT_PROJECT_PTR: + case MBT_SCOPE_TREE_PTR: case MBT_STRING: case MBT_STRING_PTR: case MBT_COUNT: break; @@ -16000,10 +16708,31 @@ InitMemoryBookTypeWidths(void) } } +void +Coda(int Sig) +{ + // NOTE(matt): I reckon this suffices because we are single-threaded. A multi-threaded system may require a volatile + // sig_atomic_t Boolean + GlobalRunning = FALSE; +} + +void +InitInterruptHandler(void) +{ + GlobalRunning = TRUE; + struct sigaction CleanExit = {}; + CleanExit.sa_handler = &Coda; + CleanExit.sa_flags = 0; + sigemptyset(&CleanExit.sa_mask); + sigaction(SIGINT, &CleanExit, 0); +} + + int main(int ArgC, char **Args) { MEM_TEST_TOP("main()"); + InitInterruptHandler(); Assert(ArrayCount(MemoryBookTypeWidths) == MBT_COUNT); Assert(ArrayCount(BufferIDStrings) == BID_COUNT); Assert(ArrayCount(TemplateTags) == TEMPLATE_TAG_COUNT); @@ -16118,9 +16847,9 @@ main(int ArgC, char **Args) if(Succeeding == RC_SUCCESS) { - if(inotifyInstance) + if(inotifyInstance != -1) { - while(MonitorFilesystem(&Neighbourhood, &CollationBuffers, &BespokeTemplate, ConfigPathL, &TokensList) != RC_ARENA_FULL) + while(GlobalRunning && MonitorFilesystem(&Neighbourhood, &CollationBuffers, &BespokeTemplate, ConfigPathL, &TokensList) != RC_ARENA_FULL) { // TODO(matt): Refetch the quotes and rebuild player pages if needed // @@ -16139,10 +16868,10 @@ main(int ArgC, char **Args) sleep(GLOBAL_UPDATE_INTERVAL); } } - else + else if(GlobalRunning) { - fprintf(stderr, "Error: Quitting because inotify initialisation failed with message\n" - " %s\n", strerror(inotifyError)); + string inotifyErrorL = Wrap0(strerror(inotifyError)); + SystemError(0, 0, S_ERROR, "inotify initialisation failed with message: ", &inotifyErrorL); } DiscardAllAndFreeConfig(); diff --git a/cinera/cinera_config.c b/cinera/cinera_config.c index 2aac7b6..31cf241 100644 --- a/cinera/cinera_config.c +++ b/cinera/cinera_config.c @@ -480,10 +480,26 @@ typedef struct { string ID; string Name; + // TODO(matt): string SortName; + string QuoteUsername; string Homepage; _memory_book(support) Support; } person; +typedef struct +{ + string ID; + string Name; + string Plural; + bool NonSpeaking; +} role; + +typedef struct +{ + person *Person; + role *Role; +} credit; + typedef struct project { string ID; @@ -515,7 +531,10 @@ typedef struct project uint64_t IconVariants; asset *IconAsset; +#if(HMMLIB_MAJOR_VERSION == 2) +#else string StreamUsername; +#endif string VODPlatform; // TODO(matt): Make this an enum bool DenyBespokeTemplates; @@ -523,9 +542,13 @@ typedef struct project bool IgnorePrivacy; person *Owner; + _memory_book(credit *) Credit; +#if(HMMLIB_MAJOR_VERSION == 2) +#else _memory_book(person *) Indexer; _memory_book(person *) Guest; _memory_book(person *) CoHost; +#endif _memory_book(medium) Medium; medium *DefaultMedium; @@ -567,6 +590,7 @@ typedef struct uint8_t LogLevel; _memory_book(person) Person; + _memory_book(role) Role; _memory_book(project) Project; memory_book ResolvedVariables; } config; @@ -669,6 +693,13 @@ Tokenise(memory_book *TokensList, string Path) } Advancement = 1; } + else if(!StringsDifferS(TokenStrings[TOKEN_MINUS], B)) + { + T.Type = TOKEN_MINUS; + T.Content.Base = B->Ptr; + ++T.Content.Length; + Advancement = 1; + } else if(!StringsDifferS(TokenStrings[TOKEN_ASSIGN], B)) { T.Type = TOKEN_ASSIGN; @@ -747,7 +778,6 @@ Tokenise(memory_book *TokensList, string Path) Advance(B, Advancement); SkipWhitespace(Result, B); } - // TODO(matt): PushConfigWatchHandle() } else { @@ -893,7 +923,10 @@ InitTypeSpecs(void) PushTypeSpecField(Root, FT_STRING, IDENT_SEARCH_LOCATION, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_SEARCH_TEMPLATE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_TEMPLATES_DIR, TRUE); +#if(HMMLIB_MAJOR_VERSION == 2) +#else PushTypeSpecField(Root, FT_STRING, IDENT_STREAM_USERNAME, TRUE); +#endif PushTypeSpecField(Root, FT_STRING, IDENT_VOD_PLATFORM, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_THEME, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_TITLE, TRUE); @@ -916,6 +949,7 @@ InitTypeSpecs(void) PushTypeSpecField(Root, FT_SCOPE, IDENT_INCLUDE, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_MEDIUM, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_PERSON, FALSE); + PushTypeSpecField(Root, FT_SCOPE, IDENT_ROLE, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_PROJECT, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_SUPPORT, FALSE); @@ -934,8 +968,22 @@ InitTypeSpecs(void) config_type_spec *Person = PushTypeSpec(&Result, IDENT_PERSON, FALSE); PushTypeSpecField(Person, FT_STRING, IDENT_NAME, TRUE); PushTypeSpecField(Person, FT_STRING, IDENT_HOMEPAGE, TRUE); +#if(HMMLIB_MAJOR_VERSION == 2) + PushTypeSpecField(Person, FT_STRING, IDENT_QUOTE_USERNAME, TRUE); +#endif PushTypeSpecField(Person, FT_SCOPE, IDENT_SUPPORT, FALSE); + config_type_spec *Role = PushTypeSpec(&Result, IDENT_ROLE, FALSE); + PushTypeSpecField(Role, FT_STRING, IDENT_NAME, TRUE); + PushTypeSpecField(Role, FT_STRING, IDENT_PLURAL, TRUE); + PushTypeSpecField(Role, FT_NUMBER, IDENT_POSITION, TRUE); + PushTypeSpecField(Role, FT_BOOLEAN, IDENT_NON_SPEAKING, TRUE); + +#if(HMMLIB_MAJOR_VERSION == 2) + config_type_spec *Credit = PushTypeSpec(&Result, IDENT_CREDIT, FALSE); + PushTypeSpecField(Credit, FT_STRING, IDENT_ROLE, FALSE); +#endif + config_type_spec *Medium = PushTypeSpec(&Result, IDENT_MEDIUM, FALSE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON, TRUE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON_NORMAL, TRUE); @@ -949,21 +997,33 @@ InitTypeSpecs(void) config_type_spec *Project = PushTypeSpec(&Result, IDENT_PROJECT, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_BASE_DIR, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_BASE_URL, TRUE); - PushTypeSpecField(Project, FT_STRING, IDENT_COHOST, FALSE); +#if(HMMLIB_MAJOR_VERSION == 2) +#else + PushTypeSpecField(Project, FT_STRING, IDENT_COHOST, FALSE); // TODO(matt): Remove +#endif PushTypeSpecField(Project, FT_STRING, IDENT_DEFAULT_MEDIUM, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_GENRE, TRUE); - PushTypeSpecField(Project, FT_STRING, IDENT_GUEST, FALSE); +#if(HMMLIB_MAJOR_VERSION == 2) +#else + PushTypeSpecField(Project, FT_STRING, IDENT_GUEST, FALSE); // TODO(matt): Remove +#endif PushTypeSpecField(Project, FT_STRING, IDENT_HMML_DIR, TRUE); - PushTypeSpecField(Project, FT_STRING, IDENT_INDEXER, FALSE); +#if(HMMLIB_MAJOR_VERSION == 2) +#else + PushTypeSpecField(Project, FT_STRING, IDENT_INDEXER, FALSE); // TODO(matt): Remove +#endif PushTypeSpecField(Project, FT_STRING, IDENT_NUMBERING_SCHEME, TRUE); - PushTypeSpecField(Project, FT_STRING, IDENT_OWNER, TRUE); + PushTypeSpecField(Project, FT_STRING, IDENT_OWNER, TRUE); // NOTE(matt): Do not remove, because ResolveLocalVariable() recognises it PushTypeSpecField(Project, FT_STRING, IDENT_PLAYER_LOCATION, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_PLAYER_TEMPLATE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_QUERY_STRING, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_SEARCH_LOCATION, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_SEARCH_TEMPLATE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_TEMPLATES_DIR, TRUE); +#if(HMMLIB_MAJOR_VERSION == 2) +#else PushTypeSpecField(Project, FT_STRING, IDENT_STREAM_USERNAME, TRUE); +#endif PushTypeSpecField(Project, FT_STRING, IDENT_VOD_PLATFORM, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_THEME, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_TITLE, TRUE); @@ -995,6 +1055,9 @@ InitTypeSpecs(void) PushTypeSpecField(Project, FT_BOOLEAN, IDENT_SINGLE_BROWSER_TAB, TRUE); // +#if(HMMLIB_MAJOR_VERSION == 2) + PushTypeSpecField(Project, FT_SCOPE, IDENT_CREDIT, FALSE); +#endif PushTypeSpecField(Project, FT_SCOPE, IDENT_INCLUDE, FALSE); PushTypeSpecField(Project, FT_SCOPE, IDENT_MEDIUM, FALSE); PushTypeSpecField(Project, FT_SCOPE, IDENT_PROJECT, FALSE); @@ -1045,6 +1108,30 @@ PrintTypeField(config_type_field *F, config_identifier_id ParentScopeID, int Ind --IndentationLevel; } } + else if((F->ID == IDENT_NAME) && ParentScopeID == IDENT_ROLE) + { + if(!ConfigIdentifiers[F->ID].IdentifierDescription_RoleDisplayed) + { + ++IndentationLevel; + IndentedCarriageReturn(IndentationLevel); + TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[F->ID].IdentifierDescription_Role)); + ConfigIdentifiers[F->ID].IdentifierDescription_RoleDisplayed = TRUE; + --IndentationLevel; + } + } +#if(HMMLIB_MAJOR_VERSION == 2) + else if((F->ID == IDENT_ROLE) && ParentScopeID == IDENT_CREDIT) + { + if(!ConfigIdentifiers[F->ID].IdentifierDescription_CreditDisplayed) + { + ++IndentationLevel; + IndentedCarriageReturn(IndentationLevel); + TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[F->ID].IdentifierDescription_Credit)); + ConfigIdentifiers[F->ID].IdentifierDescription_CreditDisplayed = TRUE; + --IndentationLevel; + } + } +#endif else { if(!ConfigIdentifiers[F->ID].IdentifierDescriptionDisplayed) @@ -1127,7 +1214,7 @@ typedef struct typedef struct { config_identifier_id Key; - uint64_t Value; + int64_t Value; token_position Position; } config_int_pair; @@ -1438,7 +1525,7 @@ PrintIntPair(config_int_pair *P, char *Delimiter, bool FillSyntax, uint64_t Inde default: { Colourise(CS_BLUE_BOLD); - fprintf(stderr, "%lu", P->Value); + fprintf(stderr, "%li", P->Value); } break; } Colourise(CS_END); @@ -1523,6 +1610,22 @@ PushPair(scope_tree *Parent, config_pair *P) *New = *P; } +config_pair * +GetPair(scope_tree *Parent, config_identifier_id FieldID) +{ + config_pair *Result = 0; + for(int i = 0; i < Parent->Pairs.ItemCount; ++i) + { + config_pair *This = GetPlaceInBook(&Parent->Pairs, i); + if(This->Key == FieldID) + { + Result = This; + break; + } + } + return Result; +} + void PushIntPair(scope_tree *Parent, config_int_pair *I) { @@ -1530,6 +1633,22 @@ PushIntPair(scope_tree *Parent, config_int_pair *I) *New = *I; } +config_int_pair * +GetIntPair(scope_tree *Parent, config_identifier_id FieldID) +{ + config_int_pair *Result = 0; + for(int i = 0; i < Parent->IntPairs.ItemCount; ++i) + { + config_int_pair *This = GetPlaceInBook(&Parent->IntPairs, i); + if(This->Key == FieldID) + { + Result = This; + break; + } + } + return Result; +} + void PushBoolPair(scope_tree *Parent, config_bool_pair *B) { @@ -2020,6 +2139,13 @@ PushDefaultIntPair(scope_tree *Parent, config_identifier_id Key, uint64_t Value) return PushIntPair(Parent, &IntPair); } +void +PushDefaultBoolPair(scope_tree *Parent, config_identifier_id Key, bool Value) +{ + config_bool_pair BoolPair = { .Key = Key, .Value = Value }; + return PushBoolPair(Parent, &BoolPair); +} + #define DEFAULT_PRIVACY_CHECK_INTERVAL 60 * 4 void SetDefaults(scope_tree *Root, memory_book *TypeSpecs) @@ -2037,6 +2163,11 @@ SetDefaults(scope_tree *Root, memory_book *TypeSpecs) PushDefaultPair(MediumAuthored, IDENT_ICON, Wrap0("🗪")); PushDefaultPair(MediumAuthored, IDENT_NAME, Wrap0("Chat Comment")); + scope_tree *RoleIndexer = PushDefaultScope(TypeSpecs, Root, IDENT_ROLE, Wrap0("indexer")); + PushDefaultPair(RoleIndexer, IDENT_NAME, Wrap0("Indexer")); + PushDefaultBoolPair(RoleIndexer, IDENT_NON_SPEAKING, TRUE); + PushDefaultIntPair(RoleIndexer, IDENT_POSITION, -1); + PushDefaultPair(Root, IDENT_QUERY_STRING, Wrap0("r")); PushDefaultPair(Root, IDENT_THEME, Wrap0("$origin")); PushDefaultPair(Root, IDENT_CACHE_DIR, Wrap0("$XDG_CACHE_HOME/cinera")); @@ -2244,8 +2375,14 @@ ScopeTokens(scope_tree *Tree, memory_book *TokensList, tokens *T, memory_book *T if(!ExpectToken(T, TOKEN_ASSIGN, 0)) { FreeScopeTree(Tree); return 0; } token Value = {}; + bool IsNegative = FALSE; + if(TokenIs(T, TOKEN_MINUS)) + { + IsNegative = TRUE; + ++T->CurrentIndex; + } if(!ExpectToken(T, TOKEN_NUMBER, &Value)) { FreeScopeTree(Tree); return 0; } - IntPair.Value = Value.int64_t; + IntPair.Value = IsNegative ? 0 - Value.int64_t : Value.int64_t; if(!ExpectToken(T, TOKEN_SEMICOLON, 0)) { FreeScopeTree(Tree); return 0; } @@ -2320,16 +2457,12 @@ ScopeTokens(scope_tree *Tree, memory_book *TokensList, tokens *T, memory_book *T ConfigError(&IncluderFilePath, Pair.Position.LineNumber, S_WARNING, "Unable to include file: ", &Pair.Value); } } break; - case RC_SCHEME_MIXTURE: - { - } break; case RC_SYNTAX_ERROR: { FreeScopeTree(Tree); return 0; } break; + case RC_SCHEME_MIXTURE: case RC_INVALID_IDENTIFIER: - { - } break; default: break; } } @@ -2550,25 +2683,66 @@ ResolveEnvironmentVariable(memory_book *M, string Variable) return Result; } +role * +GetRole(config *C, resolution_errors *E, config_pair *RoleID) +{ + role *Result = 0; + for(int i = 0; i < C->Role.ItemCount; ++i) + { + role *Role = GetPlaceInBook(&C->Role, i); + if(!StringsDifferCaseInsensitive(Role->ID, RoleID->Value)) + { + Result = Role; + break; + } + } + + if(!Result && !GetError(E, S_WARNING, &RoleID->Position, IDENT_ROLE)) + { + string Filepath = Wrap0(RoleID->Position.Filename); + ConfigError(&Filepath, RoleID->Position.LineNumber, S_WARNING, "Could not find role: ", &RoleID->Value); + PushError(E, S_WARNING, &RoleID->Position, IDENT_ROLE); + } + return Result; +} + +role * +GetRoleByID(config *C, string ID) +{ + role *Result = 0; + for(int i = 0; i < C->Role.ItemCount; ++i) + { + role *Role = GetPlaceInBook(&C->Role, i); + if(!StringsDifferCaseInsensitive(Role->ID, ID)) + { + Result = Role; + break; + } + } + return Result; +} + person * GetPerson(config *C, resolution_errors *E, config_pair *PersonID) { + person *Result = 0; for(int i = 0; i < C->Person.ItemCount; ++i) { person *Person = GetPlaceInBook(&C->Person, i); if(!StringsDifferCaseInsensitive(Person->ID, PersonID->Value)) { - return Person; + Result = Person; + break; } } - if(!GetError(E, S_WARNING, &PersonID->Position, IDENT_PERSON)) + if(!Result && !GetError(E, S_WARNING, &PersonID->Position, IDENT_PERSON)) { string Filepath = Wrap0(PersonID->Position.Filename); ConfigError(&Filepath, PersonID->Position.LineNumber, S_WARNING, "Could not find person: ", &PersonID->Value); PushError(E, S_WARNING, &PersonID->Position, IDENT_PERSON); } - return 0; + return Result; } string @@ -2712,27 +2886,31 @@ ResolveLocalVariable(config *C, resolution_errors *E, scope_tree *Scope, config_ case IDENT_OWNER: { bool Processed = FALSE; - for(int i = 0; i < Scope->Pairs.ItemCount; ++i) + while(!Processed && Scope) { - config_pair *Pair = GetPlaceInBook(&Scope->Pairs, i); - if(Variable == Pair->Key) + for(int i = 0; i < Scope->Pairs.ItemCount; ++i) { - if(GetPerson(C, E, Pair)) + config_pair *Pair = GetPlaceInBook(&Scope->Pairs, i); + if(Variable == Pair->Key) { - Result = ExtendStringInBook(&C->ResolvedVariables, Pair->Value); - Processed = TRUE; - break; - } - else - { - if(!GetError(E, S_WARNING, Position, Variable)) + if(GetPerson(C, E, Pair)) { - ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Owner set, but the person does not exist: ", &Pair->Value); - PushError(E, S_WARNING, Position, Variable); + Result = ExtendStringInBook(&C->ResolvedVariables, Pair->Value); + Processed = TRUE; + break; + } + else + { + if(!GetError(E, S_WARNING, Position, Variable)) + { + ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Owner set, but the person does not exist: ", &Pair->Value); + PushError(E, S_WARNING, Position, Variable); + } + Processed = TRUE; } - Processed = TRUE; } } + Scope = Scope->Parent; } if(!Processed) { @@ -2745,7 +2923,8 @@ ResolveLocalVariable(config *C, resolution_errors *E, scope_tree *Scope, config_ } break; case IDENT_PERSON: { - if(Scope->ID.Key != Variable) + // NOTE(matt): Allow scopes within a person scope, e.g. support, to resolve $person + while(Scope && Scope->ID.Key != Variable) { Scope = Scope->Parent; } @@ -3123,8 +3302,21 @@ PushPersonOntoConfig(config *C, resolution_errors *E, config_verifiers *V, scope { This->Homepage = ResolveString(C, E, PersonTree, Pair, FALSE); } +#if(HMMLIB_MAJOR_VERSION == 2) + else if(IDENT_QUOTE_USERNAME == Pair->Key) + { + This->QuoteUsername = ResolveString(C, E, PersonTree, Pair, FALSE); + } +#endif } +#if(HMMLIB_MAJOR_VERSION == 2) + if(This->QuoteUsername.Length == 0) + { + This->QuoteUsername = This->ID; + } +#endif + This->Support = InitBook(MBT_SUPPORT, 2); for(int i = 0; i < PersonTree->Trees.ItemCount; ++i) { @@ -3133,6 +3325,35 @@ PushPersonOntoConfig(config *C, resolution_errors *E, config_verifiers *V, scope } } +void +PushRoleOntoConfig(config *C, resolution_errors *E, config_verifiers *V, scope_tree *RoleTree) +{ + role *This = MakeSpaceInBook(&C->Role); + This->ID = ResolveString(C, E, RoleTree, &RoleTree->ID, FALSE); + for(int i = 0; i < RoleTree->Pairs.ItemCount; ++i) + { + config_pair *Pair = GetPlaceInBook(&RoleTree->Pairs, i); + if(IDENT_NAME == Pair->Key) + { + This->Name = ResolveString(C, E, RoleTree, Pair, FALSE); + } + else if(IDENT_PLURAL == Pair->Key) + { + This->Plural = ResolveString(C, E, RoleTree, Pair, FALSE); + } + } + for(int i = 0; i < RoleTree->BoolPairs.ItemCount; ++i) + { + config_bool_pair *BoolPair = GetPlaceInBook(&RoleTree->BoolPairs, i); + if(IDENT_NON_SPEAKING == BoolPair->Key) + { + This->NonSpeaking = BoolPair->Value; + } + } +} + +#if(HMMLIB_MAJOR_VERSION == 2) +#else void PushPersonOntoProject(config *C, resolution_errors *E, project *P, config_pair *Actor) { @@ -3160,6 +3381,32 @@ PushPersonOntoProject(config *C, resolution_errors *E, project *P, config_pair * } } } +#endif + +void +PushCredit(config *C, resolution_errors *E, project *P, scope_tree *CreditTree) +{ + CreditTree->ID.Value = ResolveString(C, E, CreditTree, &CreditTree->ID, FALSE); + if(CreditTree->ID.Value.Base) + { + person *Person = GetPerson(C, E, &CreditTree->ID); + if(Person) + { + for(int i = 0; i < CreditTree->Pairs.ItemCount; ++i) + { + config_pair *This = GetPlaceInBook(&CreditTree->Pairs, i); + role *Role = GetRole(C, E, This); + if(Role) + { + credit *Credit = MakeSpaceInBook(&P->Credit); + // TODO(matt): Sort the P->Credit book by Person->SortName + Credit->Role = Role; + Credit->Person = Person; + } + } + } + } +} void PushProjectOntoProject(config *C, resolution_errors *E, config_verifiers *Verifiers, project *Parent, scope_tree *ProjectTree); @@ -3255,9 +3502,13 @@ PushProject(config *C, resolution_errors *E, config_verifiers *V, project *P, sc { P->Medium = InitBook(MBT_MEDIUM, 8); P->Child = InitBook(MBT_PROJECT, 8); +#if(HMMLIB_MAJOR_VERSION == 2) + P->Credit = InitBook(MBT_CREDIT, 4); +#else P->Indexer = InitBookOfPointers(MBT_PERSON_PTR, 4); P->CoHost = InitBookOfPointers(MBT_PERSON_PTR, 4); P->Guest = InitBookOfPointers(MBT_PERSON_PTR, 4); +#endif config_string_associations *HMMLDirs = &V->HMMLDirs; P->ID = ResolveString(C, E, ProjectTree, &ProjectTree->ID, FALSE); @@ -3271,12 +3522,40 @@ PushProject(config *C, resolution_errors *E, config_verifiers *V, project *P, sc ResetPen(&C->ResolvedVariables); P->Lineage = DeriveLineageOfProject(C, ProjectTree); + // NOTE(matt): Initial pass over as-yet unresolvable local variable(s) + // + for(int i = 0; i < ProjectTree->Pairs.ItemCount; ++i) + { + config_pair *This = GetPlaceInBook(&ProjectTree->Pairs, i); + switch(This->Key) + { + case IDENT_OWNER: + { + // TODO(matt): Do we need to fail completely if owner cannot be found? + P->Owner = GetPerson(C, E, This); + } break; + default: break; + } + } + // + //// + for(int i = 0; i < ProjectTree->Trees.ItemCount; ++i) { scope_tree *This = GetPlaceInBook(&ProjectTree->Trees, i); - if(This->ID.Key == IDENT_MEDIUM) + switch(This->ID.Key) { - PushMedium(C, E, V, P, This); + case IDENT_MEDIUM: + { + PushMedium(C, E, V, P, This); + } break; +#if(HMMLIB_MAJOR_VERSION == 2) + case IDENT_CREDIT: + { + PushCredit(C, E, P, This); + } break; +#endif + default: break; } } @@ -3290,15 +3569,13 @@ PushProject(config *C, resolution_errors *E, config_verifiers *V, project *P, sc { P->DefaultMedium = GetMediumFromProject(P, This->Value); } break; case IDENT_HMML_DIR: { SetUniqueHMMLDir(C, E, HMMLDirs, P, ProjectTree, This); } break; +#if(HMMLIB_MAJOR_VERSION == 2) +#else case IDENT_INDEXER: case IDENT_COHOST: case IDENT_GUEST: { PushPersonOntoProject(C, E, P, This); } break; - case IDENT_OWNER: - { - // TODO(matt): Do we need to fail completely if owner cannot be found? - P->Owner = GetPerson(C, E, This); - } break; +#endif case IDENT_PLAYER_TEMPLATE: { P->PlayerTemplatePath = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); } break; case IDENT_THEME: @@ -3371,8 +3648,11 @@ PushProject(config *C, resolution_errors *E, config_verifiers *V, project *P, sc { P->SearchTemplatePath = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); } break; case IDENT_TEMPLATES_DIR: { P->TemplatesDir = StripSlashes(ResolveString(C, E, ProjectTree, This, TRUE), P_ABS); } break; +#if(HMMLIB_MAJOR_VERSION == 2) +#else case IDENT_STREAM_USERNAME: { P->StreamUsername = ResolveString(C, E, ProjectTree, This, FALSE); } break; +#endif case IDENT_VOD_PLATFORM: { P->VODPlatform = ResolveString(C, E, ProjectTree, This, FALSE); } break; @@ -3841,7 +4121,6 @@ TypesetVariants(typography *T, uint8_t Generation, config_identifier_id Key, uin void PrintMedium(typography *T, medium *M, char *Delimiter, uint64_t Indentation, bool AppendNewline) { - // TODO(matt): Print Me! PrintStringC(CS_BLUE_BOLD, M->ID); if(M->Hidden) { @@ -3970,7 +4249,7 @@ GetRowsRequiredForPersonInfo(typography *T, person *P) } void -PrintPerson(person *P, config_identifier_id Role, typography *Typography) +PrintPerson(person *P, typography *Typography) { bool HaveInfo = P->Name.Length > 0 || P->Homepage.Length > 0 || P->Support.ItemCount > 0; fprintf(stderr, "\n" @@ -4003,6 +4282,49 @@ PrintPerson(person *P, config_identifier_id Role, typography *Typography) } } +uint8_t +GetRowsRequiredForRoleInfo(role *R) +{ + uint8_t RowsRequired = 0; + if(R->Name.Length > 0) { ++RowsRequired; } + if(R->Plural.Length > 0) { ++RowsRequired; } + int NonSpeakingLines = 1; + RowsRequired += NonSpeakingLines; + return RowsRequired; +} + +void +PrintRole(role *R, typography *Typography) +{ + bool HaveInfo = R->Name.Length > 0 || R->Plural.Length > 0; + fprintf(stderr, "\n" + "%s%s%s%s%s ", HaveInfo ? Typography->UpperLeftCorner : Typography->UpperLeft, + Typography->Horizontal, Typography->Horizontal, Typography->Horizontal, Typography->UpperRight); + PrintStringC(CS_GREEN_BOLD, R->ID); + fprintf(stderr, "\n"); + + uint8_t RowsRequired = GetRowsRequiredForRoleInfo(R); + + int IndentationLevel = 0; + bool ShouldFillSyntax = FALSE; + if(R->Name.Length > 0) + { + fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); + config_pair Name = { .Key = IDENT_NAME, .Value = R->Name }; PrintPair(&Name, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); + --RowsRequired; + } + + if(R->Plural.Length > 0) + { + fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); + config_pair Plural = { .Key = IDENT_PLURAL, .Value = R->Plural }; PrintPair(&Plural, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); + --RowsRequired; + } + fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); + config_bool_pair NonSpeaking = { .Key = IDENT_NON_SPEAKING, .Value = R->NonSpeaking }; PrintBoolPair(&NonSpeaking, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); + --RowsRequired; +} + void PrintLineage(string Lineage, bool AppendNewline) { @@ -4026,7 +4348,8 @@ GetColumnsRequiredForMedium(medium *M, typography *T) } void -PrintTitle(char *Text, int AvailableColumns) +PrintTitle(char *Text, typography *T, uint8_t Generation, int AvailableColumns, + int64_t SectionItemCount /* NOTE(matt): Use any non-0 when the section is known to have content */) { char *LeftmostChar = "╾"; char *InnerChar = "─"; @@ -4049,6 +4372,11 @@ PrintTitle(char *Text, int AvailableColumns) fprintf(stderr, "%s", InnerChar); } fprintf(stderr, "%s", RightmostChar); + if(SectionItemCount == 0) + { + //fprintf(stderr, "\n"); + CarriageReturn(T, Generation); + } } void @@ -4057,7 +4385,7 @@ PrintMedia(project *P, typography *T, uint8_t Generation, int AvailableColumns) int IndentationLevel = 0; CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); - PrintTitle("Media", AvailableColumns); + PrintTitle("Media", T, Generation, AvailableColumns, P->Medium.ItemCount); CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); //AvailableColumns -= Generation + 1; @@ -4106,6 +4434,63 @@ PrintMedia(project *P, typography *T, uint8_t Generation, int AvailableColumns) } } +void +PrintCredits(config *C, project *P, typography *T, uint8_t Generation, int IndentationLevel, int AvailableColumns) +{ + for(int i = 0; i < C->Role.ItemCount; ++i) + { + role *Role = GetPlaceInBook(&C->Role, i); + int CreditCount = 0; + for(int j = 0; j < P->Credit.ItemCount; ++j) + { + credit *Credit = GetPlaceInBook(&P->Credit, j); + if(Role == Credit->Role) + { + ++CreditCount; + } + } + + if(CreditCount > 0) + { + CarriageReturn(T, Generation); + int Alignment = 0; + Alignment += fprintf(stderr, "%s", T->Margin); + if(CreditCount == 1) + { + Alignment += PrintStringC(CS_YELLOW_BOLD, Role->Name.Length ? Role->Name : Role->ID); + } + else + { + if(Role->Plural.Length > 0) + { + Alignment += PrintStringC(CS_YELLOW_BOLD, Role->Plural); + } + else + { + Alignment += PrintStringC(CS_YELLOW_BOLD, Role->Name.Length ? Role->Name : Role->ID); + Alignment += PrintStringC(CS_YELLOW_BOLD, Wrap0("s")); + } + } + Alignment += fprintf(stderr, "%s", T->Delimiter); + bool Printed = FALSE; + for(int j = 0; j < P->Credit.ItemCount; ++j) + { + credit *Credit = GetPlaceInBook(&P->Credit, j); + if(Role == Credit->Role) + { + if(Printed) + { + CarriageReturn(T, Generation); + AlignText(Alignment); + } + PrintStringC(CS_GREEN_BOLD, Credit->Person->Name.Length ? Credit->Person->Name : Credit->Person->ID); + Printed = TRUE; + } + } + } + } +} + void TypesetPair(typography *T, uint8_t Generation, config_identifier_id Key, string Value, int AvailableColumns) { @@ -4177,7 +4562,7 @@ TypesetNumberingScheme(typography *T, uint8_t Generation, numbering_scheme N) } void -PrintProject(project *P, typography *T, int Ancestors, int IndentationLevel, int TerminalColumns) +PrintProject(config *C, project *P, typography *T, int Ancestors, int IndentationLevel, int TerminalColumns) { int Generation = Ancestors + 1; CarriageReturn(T, Ancestors); @@ -4196,7 +4581,7 @@ PrintProject(project *P, typography *T, int Ancestors, int IndentationLevel, int CarriageReturn(T, Generation); CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); - PrintTitle("Settings", AvailableColumns); + PrintTitle("Settings", T, Generation, AvailableColumns, -1); TypesetPair(T, Generation, IDENT_TITLE, P->Title, AvailableColumns); TypesetPair(T, Generation, IDENT_HTML_TITLE, P->HTMLTitle, AvailableColumns); @@ -4213,7 +4598,10 @@ PrintProject(project *P, typography *T, int Ancestors, int IndentationLevel, int TypesetPair(T, Generation, IDENT_BASE_URL, P->BaseURL, AvailableColumns); TypesetPair(T, Generation, IDENT_SEARCH_LOCATION, P->SearchLocation, AvailableColumns); TypesetPair(T, Generation, IDENT_PLAYER_LOCATION, P->PlayerLocation, AvailableColumns); +#if(HMMLIB_MAJOR_VERSION == 2) +#else TypesetPair(T, Generation, IDENT_STREAM_USERNAME, P->StreamUsername, AvailableColumns); +#endif TypesetPair(T, Generation, IDENT_VOD_PLATFORM, P->VODPlatform, AvailableColumns); TypesetPair(T, Generation, IDENT_THEME, P->Theme, AvailableColumns); @@ -4233,11 +4621,10 @@ PrintProject(project *P, typography *T, int Ancestors, int IndentationLevel, int TypesetBool(T, Generation, IDENT_IGNORE_PRIVACY, P->IgnorePrivacy); TypesetBool(T, Generation, IDENT_SINGLE_BROWSER_TAB, P->SingleBrowserTab); - if(P->Owner) - { - TypesetPair(T, Generation, IDENT_OWNER, P->Owner->ID, AvailableColumns); - } + TypesetPair(T, Generation, IDENT_OWNER, P->Owner ? P->Owner->ID : Wrap0(""), AvailableColumns); +#if(HMMLIB_MAJOR_VERSION == 2) +#else for(int i = 0; i < P->Indexer.ItemCount; ++i) { person **Indexer = GetPlaceInBook(&P->Indexer, i); @@ -4253,17 +4640,32 @@ PrintProject(project *P, typography *T, int Ancestors, int IndentationLevel, int person **Guest = GetPlaceInBook(&P->Guest, i); TypesetPair(T, Generation, IDENT_GUEST, (*Guest)->ID, AvailableColumns); } +#endif + + CarriageReturn(T, Generation); + CarriageReturn(T, Generation); + fprintf(stderr, "%s", T->Margin); + PrintTitle("Credits", T, Generation, AvailableColumns, P->Credit.ItemCount); + if(P->Credit.ItemCount > 0) + { + PrintCredits(C, P, T, Generation, IndentationLevel, AvailableColumns); + } + else + { + fprintf(stderr, "%s", T->Margin); + PrintC(CS_YELLOW, "[none]"); + } if(P->Child.ItemCount) { CarriageReturn(T, Generation); CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); - PrintTitle("Children", AvailableColumns); + PrintTitle("Children", T, Generation, AvailableColumns, -1); ++Ancestors; for(int i = 0; i < P->Child.ItemCount; ++i) { - PrintProject(GetPlaceInBook(&P->Child, i), T, Ancestors, IndentationLevel, TerminalColumns); + PrintProject(C, GetPlaceInBook(&P->Child, i), T, Ancestors, IndentationLevel, TerminalColumns); } --Ancestors; } @@ -4296,7 +4698,7 @@ PrintConfig(config *C, bool ShouldClearTerminal) // separate fprintf(stderr, "\n"); - PrintTitle("Global Settings", TermCols); + PrintTitle("Global Settings", &Typography, 0, TermCols, -1); int IndentationLevel = 0; int AvailableColumns = TermCols - StringLength(Typography.Margin); @@ -4330,17 +4732,24 @@ PrintConfig(config *C, bool ShouldClearTerminal) PrintLogLevel(ConfigIdentifiers[IDENT_LOG_LEVEL].String, C->LogLevel, Typography.Delimiter, IndentationLevel, TRUE); fprintf(stderr, "\n"); - PrintTitle("People", TermCols); + PrintTitle("People", &Typography, 0, TermCols, C->Person.ItemCount); for(int i = 0; i < C->Person.ItemCount; ++i) { - PrintPerson(GetPlaceInBook(&C->Person, i), IDENT_NULL, &Typography); + PrintPerson(GetPlaceInBook(&C->Person, i), &Typography); } fprintf(stderr, "\n"); - PrintTitle("Projects", TermCols); + PrintTitle("Roles", &Typography, 0, TermCols, C->Role.ItemCount); + for(int i = 0; i < C->Role.ItemCount; ++i) + { + PrintRole(GetPlaceInBook(&C->Role, i), &Typography); + } + + fprintf(stderr, "\n"); + PrintTitle("Projects", &Typography, 0, TermCols, -1); for(int i = 0; i < C->Project.ItemCount; ++i) { - PrintProject(GetPlaceInBook(&C->Project, i), &Typography, 0, IndentationLevel, TermCols); + PrintProject(C, GetPlaceInBook(&C->Project, i), &Typography, 0, IndentationLevel, TermCols); } } } @@ -4349,10 +4758,13 @@ void FreeProject(project *P) { FreeBook(&P->Medium); - +#if(HMMLIB_MAJOR_VERSION == 2) + FreeBook(&P->Credit); +#else FreeBook(&P->Indexer); FreeBook(&P->Guest); FreeBook(&P->CoHost); +#endif for(int i = 0; i < P->Child.ItemCount; ++i) { FreeProject(GetPlaceInBook(&P->Child, i)); @@ -4373,6 +4785,7 @@ FreeProject(project *P) FreeBook(&Person->Support);\ }\ FreeBook(&C->Person);\ + FreeBook(&C->Role);\ for(int i = 0; i < C->Project.ItemCount; ++i)\ {\ FreeProject(GetPlaceInBook(&C->Project, i));\ @@ -4404,6 +4817,93 @@ InitVerifiers(void) return Result; } +bool +IsPositioned(config_int_pair *Position) +{ + return Position && Position->Value != 0; +} + +void +PositionRolesInConfig(config *C, resolution_errors *E, config_verifiers *V, scope_tree *S) +{ + memory_book RolesStaging = InitBookOfPointers(MBT_SCOPE_TREE_PTR, 4); + for(int i = 0; i < S->Trees.ItemCount; ++i) + { + scope_tree *Tree = GetPlaceInBook(&S->Trees, i); + if(Tree->ID.Key == IDENT_ROLE) + { + scope_tree **This = MakeSpaceInBook(&RolesStaging); + *This = Tree; + } + } + + int SlotCount = RolesStaging.ItemCount; + scope_tree *RoleSlot[SlotCount]; + Clear(RoleSlot, sizeof(scope_tree *) * SlotCount); + for(int i = 0; i < SlotCount; ++i) + { + scope_tree **Role = GetPlaceInBook(&RolesStaging, i); + config_int_pair *Position = GetIntPair(*Role, IDENT_POSITION); + if(IsPositioned(Position)) + { + int TargetPos; + if(Position->Value < 0) + { + TargetPos = SlotCount + Position->Value; + Clamp(0, TargetPos, SlotCount - 1); + while(RoleSlot[TargetPos] && TargetPos > 0) + { + --TargetPos; + } + + while(RoleSlot[TargetPos] && TargetPos < SlotCount - 1) + { + ++TargetPos; + } + } + else if(Position->Value > 0) + { + TargetPos = Position->Value - 1; + Clamp(0, TargetPos, SlotCount - 1); + while(RoleSlot[TargetPos] && TargetPos < SlotCount - 1) + { + ++TargetPos; + } + + while(RoleSlot[TargetPos] && TargetPos > 0) + { + --TargetPos; + } + } + RoleSlot[TargetPos] = *Role; + } + } + + for(int i = 0; i < SlotCount; ++i) + { + scope_tree **Role = GetPlaceInBook(&RolesStaging, i); + config_int_pair *Position = GetIntPair(*Role, IDENT_POSITION); + if(!IsPositioned(Position)) + { + for(int j = 0; j < SlotCount; ++j) + { + if(!RoleSlot[j]) + { + RoleSlot[j] = *Role; + break; + } + } + } + } + + for(int i = 0; i < SlotCount; ++i) + { + PushRoleOntoConfig(C, E, V, RoleSlot[i]); + } + + FreeBook(&RolesStaging); +} + config * ResolveVariables(scope_tree *S) { @@ -4411,6 +4911,7 @@ ResolveVariables(scope_tree *S) config *Result = calloc(1, sizeof(config)); Result->ResolvedVariables = InitBookOfStrings(Kilobytes(1)); Result->Person = InitBook(MBT_PERSON, 8); + Result->Role = InitBook(MBT_ROLE, 4); Result->Project = InitBook(MBT_PROJECT, 8); resolution_errors Errors = {}; @@ -4553,6 +5054,8 @@ ResolveVariables(scope_tree *S) } } + PositionRolesInConfig(Result, &Errors, &Verifiers, S); + for(int i = 0; i < S->Trees.ItemCount; ++i) { scope_tree *Tree = GetPlaceInBook(&S->Trees, i);