diff --git a/src/act.comm.c b/src/act.comm.c index 376a67c..6e470ed 100644 --- a/src/act.comm.c +++ b/src/act.comm.c @@ -110,38 +110,374 @@ static void to_second_person_self(const char *in, char *out, size_t outlen) { } } +static void trim_whitespace(char *s) { + char *start = s; + while (*start && isspace((unsigned char)*start)) + start++; + + if (start != s) + memmove(s, start, strlen(start) + 1); + + size_t len = strlen(s); + while (len > 0 && isspace((unsigned char)s[len - 1])) + s[--len] = '\0'; +} + ACMD(do_say) { - skip_spaces(&argument); + char *p = argument; + char bracket_raw[MAX_INPUT_LENGTH] = ""; + char paren_raw[MAX_INPUT_LENGTH] = ""; + char speech[MAX_INPUT_LENGTH]; + struct targeted_phrase bracket_phrase; + struct targeted_phrase paren_phrase; + bool has_bracket = FALSE; + bool has_paren = FALSE; - if (!*argument) + skip_spaces(&p); + + if (*p == '[') { + const char *close = strchr(p, ']'); + if (!close) { + send_to_char(ch, "You need a closing ']'.\r\n"); + return; + } + size_t len = (size_t)(close - p - 1); + if (len >= sizeof(bracket_raw)) + len = sizeof(bracket_raw) - 1; + strncpy(bracket_raw, p + 1, len); + bracket_raw[len] = '\0'; + trim_whitespace(bracket_raw); + p = (char *)close + 1; + } + + skip_spaces(&p); + + if (*p == '(') { + const char *close = strchr(p, ')'); + if (!close) { + send_to_char(ch, "You need a closing ')'.\r\n"); + return; + } + size_t len = (size_t)(close - p - 1); + if (len >= sizeof(paren_raw)) + len = sizeof(paren_raw) - 1; + strncpy(paren_raw, p + 1, len); + paren_raw[len] = '\0'; + trim_whitespace(paren_raw); + p = (char *)close + 1; + } + + skip_spaces(&p); + + if (!*p) { send_to_char(ch, "Yes, but WHAT do you want to say?\r\n"); - else { - char buf[MAX_INPUT_LENGTH + 14], *msg; - struct char_data *vict; - - if (CONFIG_SPECIAL_IN_COMM && legal_communication(argument)) - parse_at(argument); + return; + } - snprintf(buf, sizeof(buf), "$n\tn says, '%s'", argument); - msg = act(buf, FALSE, ch, 0, 0, TO_ROOM | DG_NO_TRIG); + strlcpy(speech, p, sizeof(speech)); - for (vict = world[IN_ROOM(ch)].people; vict; vict = vict->next_in_room) - if (vict != ch && GET_POS(vict) > POS_SLEEPING) - add_history(vict, msg, HIST_SAY); + if (CONFIG_SPECIAL_IN_COMM && legal_communication(speech)) + parse_at(speech); - if (!IS_NPC(ch) && PRF_FLAGGED(ch, PRF_NOREPEAT)) - send_to_char(ch, "%s", CONFIG_OK); - else { - sprintf(buf, "You say, '%s'", argument); - msg = act(buf, FALSE, ch, 0, 0, TO_CHAR | DG_NO_TRIG); - add_history(ch, msg, HIST_SAY); + if (*bracket_raw) { + if (!build_targeted_phrase(ch, bracket_raw, FALSE, &bracket_phrase)) + return; + has_bracket = TRUE; + } + if (*paren_raw) { + if (!build_targeted_phrase(ch, paren_raw, FALSE, &paren_phrase)) + return; + has_paren = TRUE; + } + + bool suppress_self = (!IS_NPC(ch) && PRF_FLAGGED(ch, PRF_NOREPEAT)); + bool use_say = (has_bracket || has_paren); + + for (struct char_data *vict = world[IN_ROOM(ch)].people; vict; vict = vict->next_in_room) { + bool self = (vict == ch); + + if (self && suppress_self) + continue; + if (!self && GET_POS(vict) <= POS_SLEEPING) + continue; + + char prefix[MAX_STRING_LENGTH] = ""; + char suffix[MAX_STRING_LENGTH] = ""; + char first_line[MAX_STRING_LENGTH]; + char speaker[MAX_INPUT_LENGTH]; + + if (has_bracket) + render_targeted_phrase(ch, &bracket_phrase, FALSE, vict, prefix, sizeof(prefix)); + if (has_paren) + render_targeted_phrase(ch, &paren_phrase, FALSE, vict, suffix, sizeof(suffix)); + + if (self) + strlcpy(speaker, "You", sizeof(speaker)); + else + strlcpy(speaker, PERS(ch, vict), sizeof(speaker)); + + first_line[0] = '\0'; + strlcpy(first_line, "", sizeof(first_line)); + if (*prefix) { + char capped[MAX_STRING_LENGTH]; + strlcpy(capped, prefix, sizeof(capped)); + CAP(capped); + strlcpy(first_line, capped, sizeof(first_line)); + strlcat(first_line, ", ", sizeof(first_line)); + strlcat(first_line, speaker, sizeof(first_line)); + } else { + strlcpy(first_line, speaker, sizeof(first_line)); + } + + strlcat(first_line, (self && use_say) ? " say" : " says", sizeof(first_line)); + + if (*suffix) { + strlcat(first_line, ", ", sizeof(first_line)); + strlcat(first_line, suffix, sizeof(first_line)); + } + + strlcat(first_line, ":", sizeof(first_line)); + send_to_char(vict, "%s\r\n \"%s\"\r\n", first_line, speech); + + if (!self || !suppress_self) { + char hist_buf[MAX_STRING_LENGTH]; + snprintf(hist_buf, sizeof(hist_buf), "%s\r\n \"%s\"", first_line, speech); + add_history(vict, hist_buf, HIST_SAY); } } - /* Trigger check. */ - speech_mtrigger(ch, argument); - speech_wtrigger(ch, argument); + if (suppress_self) + send_to_char(ch, "%s", CONFIG_OK); + + speech_mtrigger(ch, speech); + speech_wtrigger(ch, speech); +} + +ACMD(do_talk) +{ + struct obj_data *furniture = SITTING(ch); + int allowed_positions = 0; + char *p = argument; + char bracket_raw[MAX_INPUT_LENGTH] = ""; + char paren_raw[MAX_INPUT_LENGTH] = ""; + struct targeted_phrase bracket_phrase; + struct targeted_phrase paren_phrase; + bool has_bracket = FALSE, has_paren = FALSE; + + if (!furniture || GET_OBJ_TYPE(furniture) != ITEM_FURNITURE) { + send_to_char(ch, "You need to be seated at a piece of furniture to talk there.\r\n"); + return; + } + + if (GET_POS(ch) != POS_SITTING) { + send_to_char(ch, "You need to be sitting first.\r\n"); + return; + } + + allowed_positions = GET_OBJ_VAL(furniture, VAL_FURN_POSITIONS); + if (allowed_positions > 0 && !(allowed_positions & (1 << 1))) { + send_to_char(ch, "That furniture doesn't have any seats.\r\n"); + return; + } + + skip_spaces(&p); + if (*p == '[') { + const char *close = strchr(p, ']'); + if (!close) { + send_to_char(ch, "You need a closing ']'.\r\n"); + return; + } + size_t len = (size_t)(close - p - 1); + if (len >= sizeof(bracket_raw)) + len = sizeof(bracket_raw) - 1; + strncpy(bracket_raw, p + 1, len); + bracket_raw[len] = '\0'; + trim_whitespace(bracket_raw); + p = (char *)close + 1; + } + + skip_spaces(&p); + + if (*p == '(') { + const char *close = strchr(p, ')'); + if (!close) { + send_to_char(ch, "You need a closing ')'.\r\n"); + return; + } + size_t len = (size_t)(close - p - 1); + if (len >= sizeof(paren_raw)) + len = sizeof(paren_raw) - 1; + strncpy(paren_raw, p + 1, len); + paren_raw[len] = '\0'; + trim_whitespace(paren_raw); + p = (char *)close + 1; + } + + skip_spaces(&p); + + if (!*p) { + send_to_char(ch, "Talk what?\r\n"); + return; + } + + char speech[MAX_INPUT_LENGTH]; + strlcpy(speech, p, sizeof(speech)); + + if (CONFIG_SPECIAL_IN_COMM && legal_communication(speech)) + parse_at(speech); + + if (*bracket_raw) { + if (!build_targeted_phrase(ch, bracket_raw, FALSE, &bracket_phrase)) + return; + has_bracket = TRUE; + } + if (*paren_raw) { + if (!build_targeted_phrase(ch, paren_raw, FALSE, &paren_phrase)) + return; + has_paren = TRUE; + } + + const char *furn_name = (furniture->short_description && *furniture->short_description) + ? furniture->short_description : "the furniture"; + + bool suppress_self = (!IS_NPC(ch) && PRF_FLAGGED(ch, PRF_NOREPEAT)); + bool delivered = FALSE; + + for (struct char_data *tch = OBJ_SAT_IN_BY(furniture); tch; tch = NEXT_SITTING(tch)) { + if (tch == ch) + continue; + if (SITTING(tch) != furniture) + continue; + if (GET_POS(tch) != POS_SITTING) + continue; + if (GET_POS(tch) <= POS_SLEEPING) + continue; + + char prefix[MAX_STRING_LENGTH] = ""; + char suffix[MAX_STRING_LENGTH] = ""; + char first_line[MAX_STRING_LENGTH]; + const char *speaker = PERS(ch, tch); + + if (has_bracket) + render_targeted_phrase(ch, &bracket_phrase, FALSE, tch, prefix, sizeof(prefix)); + if (has_paren) + render_targeted_phrase(ch, &paren_phrase, FALSE, tch, suffix, sizeof(suffix)); + + first_line[0] = '\0'; + if (*prefix) { + char capped[MAX_STRING_LENGTH]; + strlcpy(capped, prefix, sizeof(capped)); + CAP(capped); + strlcpy(first_line, capped, sizeof(first_line)); + strlcat(first_line, ", ", sizeof(first_line)); + strlcat(first_line, speaker, sizeof(first_line)); + } else { + strlcpy(first_line, speaker, sizeof(first_line)); + } + + strlcat(first_line, " says", sizeof(first_line)); + strlcat(first_line, ", ", sizeof(first_line)); + if (*suffix) { + strlcat(first_line, suffix, sizeof(first_line)); + strlcat(first_line, ", ", sizeof(first_line)); + } + char locbuf[MAX_INPUT_LENGTH]; + snprintf(locbuf, sizeof(locbuf), "at %s,", furn_name); + strlcat(first_line, locbuf, sizeof(first_line)); + + send_to_char(tch, "%s\r\n \"%s\"\r\n", first_line, speech); + + char hist_buf[MAX_STRING_LENGTH]; + snprintf(hist_buf, sizeof(hist_buf), "%s\r\n \"%s\"", first_line, speech); + add_history(tch, hist_buf, HIST_SAY); + delivered = TRUE; + } + + if (!delivered) + send_to_char(ch, "No one else seated there hears you.\r\n"); + + if (suppress_self) + send_to_char(ch, "%s", CONFIG_OK); + else { + char prefix[MAX_STRING_LENGTH] = ""; + char suffix[MAX_STRING_LENGTH] = ""; + char first_line[MAX_STRING_LENGTH]; + + if (has_bracket) + render_targeted_phrase(ch, &bracket_phrase, FALSE, ch, prefix, sizeof(prefix)); + if (has_paren) + render_targeted_phrase(ch, &paren_phrase, FALSE, ch, suffix, sizeof(suffix)); + + if (*prefix) { + char capped[MAX_STRING_LENGTH]; + strlcpy(capped, prefix, sizeof(capped)); + CAP(capped); + strlcpy(first_line, capped, sizeof(first_line)); + strlcat(first_line, ", you", sizeof(first_line)); + } else + strlcpy(first_line, "you", sizeof(first_line)); + + strlcat(first_line, " say", sizeof(first_line)); + strlcat(first_line, ", ", sizeof(first_line)); + if (*suffix) { + strlcat(first_line, suffix, sizeof(first_line)); + strlcat(first_line, ", ", sizeof(first_line)); + } + char locbuf[MAX_INPUT_LENGTH]; + snprintf(locbuf, sizeof(locbuf), "at %s,", furn_name); + strlcat(first_line, locbuf, sizeof(first_line)); + + send_to_char(ch, "%s\r\n \"%s\"\r\n", first_line, speech); + char hist_buf[MAX_STRING_LENGTH]; + snprintf(hist_buf, sizeof(hist_buf), "%s\r\n \"%s\"", first_line, speech); + add_history(ch, hist_buf, HIST_SAY); + } + + /* Notify others in the room (not seated at this furniture) with an action cue. */ + for (struct char_data *onlooker = world[IN_ROOM(ch)].people; onlooker; onlooker = onlooker->next_in_room) { + if (onlooker == ch) + continue; + if (GET_POS(onlooker) <= POS_SLEEPING) + continue; + if (SITTING(onlooker) == furniture && GET_POS(onlooker) == POS_SITTING) + continue; /* already heard the speech */ + + char prefix[MAX_STRING_LENGTH] = ""; + char suffix[MAX_STRING_LENGTH] = ""; + char line[MAX_STRING_LENGTH]; + const char *speaker = PERS(ch, onlooker); + + if (has_bracket) + render_targeted_phrase(ch, &bracket_phrase, FALSE, onlooker, prefix, sizeof(prefix)); + if (has_paren) + render_targeted_phrase(ch, &paren_phrase, FALSE, onlooker, suffix, sizeof(suffix)); + + line[0] = '\0'; + if (*prefix) { + char capped[MAX_STRING_LENGTH]; + strlcpy(capped, prefix, sizeof(capped)); + CAP(capped); + strlcpy(line, capped, sizeof(line)); + strlcat(line, ", ", sizeof(line)); + strlcat(line, speaker, sizeof(line)); + } else + strlcpy(line, speaker, sizeof(line)); + + strlcat(line, " says something at ", sizeof(line)); + strlcat(line, furn_name, sizeof(line)); + + if (*suffix) { + strlcat(line, ", ", sizeof(line)); + strlcat(line, suffix, sizeof(line)); + } + strlcat(line, ".", sizeof(line)); + + send_to_char(onlooker, "%s\r\n", line); + } + + speech_mtrigger(ch, speech); + speech_wtrigger(ch, speech); } ACMD(do_ooc) diff --git a/src/act.h b/src/act.h index 04c7ac3..c800b71 100644 --- a/src/act.h +++ b/src/act.h @@ -20,6 +20,23 @@ #include "utils.h" /* for the ACMD macro */ +#ifndef MAX_EMOTE_TOKENS +#define MAX_EMOTE_TOKENS 16 +#endif + +struct emote_token { + char op; + char name[MAX_NAME_LENGTH]; + struct char_data *tch; + struct obj_data *tobj; +}; + +struct targeted_phrase { + char template[MAX_STRING_LENGTH]; + int token_count; + struct emote_token tokens[MAX_EMOTE_TOKENS]; +}; + /***************************************************************************** * Begin Functions and defines for act.comm.c ****************************************************************************/ @@ -44,6 +61,9 @@ ACMD(do_page); ACMD(do_reply); ACMD(do_tell); ACMD(do_write); +ACMD(do_talk); +bool build_targeted_phrase(struct char_data *ch, const char *input, bool allow_actor_at, struct targeted_phrase *phrase); +void render_targeted_phrase(struct char_data *actor, const struct targeted_phrase *phrase, bool actor_possessive_for_at, struct char_data *viewer, char *out, size_t outsz); /***************************************************************************** * Begin Functions and defines for act.informative.c ****************************************************************************/ diff --git a/src/act.wizard.c b/src/act.wizard.c index ea41640..a1f4446 100644 --- a/src/act.wizard.c +++ b/src/act.wizard.c @@ -589,7 +589,7 @@ static int purge_room(room_rnum room) /* ===================== Emote engine ===================== */ /* Operators: - * + * * ~ (name) / target sees "you" * ! him/her/them / target sees "you" * % (name)'s / target sees "your" @@ -601,10 +601,6 @@ static int purge_room(room_rnum room) * @ moves actor name (or actor's possessive for pemote) to that position */ -#ifndef MAX_EMOTE_TOKENS -#define MAX_EMOTE_TOKENS 16 -#endif - /* --- Pronoun & string helpers --- */ static const char *pron_obj(struct char_data *tch) { /* him/her/them */ switch (GET_SEX(tch)) { case SEX_MALE: return "him"; case SEX_FEMALE: return "her"; default: return "them"; } @@ -676,6 +672,15 @@ static void replace_all_tokens(char *hay, size_t haysz, const char *needle, cons strlcpy(hay, work, haysz); } +static bool is_token_operator(char c) { + switch (c) { + case '~': case '!': case '%': case '^': case '#': + case '&': case '=': case '+': case '@': + return TRUE; + } + return FALSE; +} + /* Capitalize the first alphabetic character of every sentence (start and after .?!). Skips any number of spaces and common closers (quotes/brackets) between sentences. */ static void capitalize_sentences(char *s) { @@ -762,16 +767,8 @@ static bool resolve_reference(struct char_data *actor, return false; } -/* --- Token model --- */ -struct emote_tok { - char op; /* one of ~ ! % ^ # & = + or '@' */ - char name[MAX_NAME_LENGTH]; /* raw token text (empty for '@') */ - struct char_data *tch; /* resolved character (if any) */ - struct obj_data *tobj; /* resolved object (if any) */ -}; - /* Build replacement text for a token as seen by 'viewer'. */ -static void build_replacement(const struct emote_tok *tok, +static void build_replacement(const struct emote_token *tok, struct char_data *actor, struct char_data *viewer, bool actor_possessive_for_at, @@ -867,6 +864,130 @@ static void build_replacement(const struct emote_tok *tok, strlcpy(out, "something", outsz); } +bool build_targeted_phrase(struct char_data *ch, const char *input, bool allow_actor_at, struct targeted_phrase *phrase) { + struct emote_token tokens[MAX_EMOTE_TOKENS]; + int tokc = 0; + char out[MAX_STRING_LENGTH]; + char working[MAX_STRING_LENGTH]; + const char *p; + + if (!phrase) + return FALSE; + + phrase->template[0] = '\0'; + phrase->token_count = 0; + + if (!input || !*input) + return TRUE; + + strlcpy(working, input, sizeof(working)); + out[0] = '\0'; + p = working; + + while (*p) { + if (is_token_operator(*p)) { + char op = *p++; + char name[MAX_NAME_LENGTH]; + int ni = 0; + + if (op == '@' && !allow_actor_at) { + send_to_char(ch, "You can't use '@' in that phrase.\r\n"); + return FALSE; + } + + if (op != '@') { + const char *q = p; + + while (*q && isdigit((unsigned char)*q) && ni < (int)sizeof(name) - 1) + name[ni++] = *q++; + + if (ni > 0 && *q == '.' && ni < (int)sizeof(name) - 1) + name[ni++] = *q++; + + while (*q && (isalnum((unsigned char)*q) || *q == '_') && ni < (int)sizeof(name) - 1) + name[ni++] = *q++; + + name[ni] = '\0'; + p = q; + } else { + name[0] = '\0'; + } + + if (tokc >= MAX_EMOTE_TOKENS) { + send_to_char(ch, "That's too many references for one phrase.\r\n"); + return FALSE; + } + + tokens[tokc].op = op; + tokens[tokc].name[0] = '\0'; + tokens[tokc].tch = NULL; + tokens[tokc].tobj = NULL; + + if (op != '@') { + if (!*name) { + send_to_char(ch, "You need to specify who or what you're referencing.\r\n"); + return FALSE; + } + strlcpy(tokens[tokc].name, name, sizeof(tokens[tokc].name)); + if (!resolve_reference(ch, name, &tokens[tokc].tch, &tokens[tokc].tobj)) { + send_to_char(ch, "You can't find one of the references here.\r\n"); + return FALSE; + } + } + + char ph[16]; + snprintf(ph, sizeof(ph), "$T%d", tokc + 1); + strlcat(out, ph, sizeof(out)); + tokc++; + continue; + } + + char buf[2] = { *p++, '\0' }; + strlcat(out, buf, sizeof(out)); + } + + strlcpy(phrase->template, out, sizeof(phrase->template)); + phrase->token_count = tokc; + for (int i = 0; i < tokc; i++) + phrase->tokens[i] = tokens[i]; + + return TRUE; +} + +void render_targeted_phrase(struct char_data *actor, + const struct targeted_phrase *phrase, + bool actor_possessive_for_at, + struct char_data *viewer, + char *out, + size_t outsz) +{ + char msg[MAX_STRING_LENGTH]; + + if (!out || !phrase) { + if (out && outsz > 0) + *out = '\0'; + return; + } + + if (!phrase->template[0]) { + if (outsz > 0) + *out = '\0'; + return; + } + + strlcpy(msg, phrase->template, sizeof(msg)); + + for (int i = 0; i < phrase->token_count; i++) { + char token[16], repl[MAX_INPUT_LENGTH]; + snprintf(token, sizeof(token), "$T%d", i + 1); + build_replacement(&phrase->tokens[i], actor, viewer, actor_possessive_for_at, repl, sizeof(repl)); + replace_all_tokens(msg, sizeof(msg), token, repl); + } + + collapse_spaces(msg); + strlcpy(out, msg, outsz); +} + static bool hidden_emote_can_view(struct char_data *actor, struct char_data *viewer, int stealth_total) { @@ -894,7 +1015,7 @@ void perform_emote(struct char_data *ch, char *argument, bool possessive, bool h int at_count = 0; int stealth_total = 0; - struct emote_tok toks[MAX_EMOTE_TOKENS]; + struct emote_token toks[MAX_EMOTE_TOKENS]; int tokc = 0; skip_spaces(&argument); @@ -993,6 +1114,12 @@ void perform_emote(struct char_data *ch, char *argument, bool possessive, bool h collapse_spaces(with_placeholders); } + struct targeted_phrase phrase; + strlcpy(phrase.template, with_placeholders, sizeof(phrase.template)); + phrase.token_count = tokc; + for (int i = 0; i < tokc && i < MAX_EMOTE_TOKENS; i++) + phrase.tokens[i] = toks[i]; + /* Deliver personalized message to everyone in the room (including actor) */ for (struct descriptor_data *d = descriptor_list; d; d = d->next) { if (STATE(d) != CON_PLAYING || !d->character) continue; @@ -1001,17 +1128,7 @@ void perform_emote(struct char_data *ch, char *argument, bool possessive, bool h continue; char msg[MAX_STRING_LENGTH]; - strlcpy(msg, with_placeholders, sizeof(msg)); - - bool actor_poss_for_at = possessive; - - /* Replace each $Tn with viewer-specific text */ - for (int i = 0; i < tokc; i++) { - char token[16], repl[MAX_INPUT_LENGTH]; - snprintf(token, sizeof(token), "$T%d", i + 1); - build_replacement(&toks[i], ch, d->character, actor_poss_for_at, repl, sizeof(repl)); - replace_all_tokens(msg, sizeof(msg), token, repl); - } + render_targeted_phrase(ch, &phrase, possessive, d->character, msg, sizeof(msg)); /* Final per-viewer cleanup: spaces + multi-sentence capitalization */ collapse_spaces(msg); diff --git a/src/interpreter.c b/src/interpreter.c index ff01474..c1e245c 100644 --- a/src/interpreter.c +++ b/src/interpreter.c @@ -293,6 +293,7 @@ cpp_extern const struct command_info cmd_info[] = { { "switch" , "switch" , POS_DEAD , do_switch , LVL_GOD, 0 }, { "tell" , "t" , POS_DEAD , do_tell , LVL_IMMORT, 0 }, + { "talk" , "talk" , POS_SITTING , do_talk , 0, 0 }, { "take" , "ta" , POS_RESTING , do_get , 0, 0 }, { "taste" , "tas" , POS_RESTING , do_eat , 0, SCMD_TASTE }, { "teleport" , "tele" , POS_DEAD , do_teleport , LVL_BUILDER, 0 },