// ============================================================ // THE ASSEMBLY ROOMS DANCE CARD SYSTEM v2.0 // Assembly Rooms Board - for OpenSimulator // "Conducted with Propriety" // ------------------------------------------------------------ // The single "brain" of the ball. Rez ONE per region. // - Reads the "Ball Program" notecard (settings + dance list) // - Registers dancers: ladies wear the wrist Dance Card, // gentlemen wear the Pocket Program (HUD) // - Brokers every dance request and enforces etiquette // - MC controls: start / next / previous dance, announcements // - Draws the program on its face (osSetDynamicTextureData) // - Saves the ball to a "Ball Save" notecard (osMakeNotecard) // // Inclusive Lead / Follow / Either roles. // NO hover text. NO MOAP. // // Display notes (house rules learned the hard way): // - Named colours only (no hex) // - No FontName (default font) // - Background = prim face colour, full bright ON // (use a light colour: Ivory / Cream / Antique White) // ============================================================ string PROGRAM_CARD = "Ball Program"; string SAVE_CARD = "Ball Save"; integer BOARD_CH = -77310; // must match the Dance Card script integer MAX_PROGRAM = 20; // most dances a program may hold // ---------- settings (overridden by the Ball Program notecard) ---------- string BALL_NAME = "The Grand Ball"; integer MAX_PER_PARTNER = 2; integer MAX_DANCES = 0; // 0 = no cap beyond the program length integer REQUEST_TIMEOUT = 60; // seconds a lady/gent has to answer list MC_NAMES = []; // lower-case avatar names (owner is always MC) string CARD_ITEM = "Assembly Rooms Dance Card"; string HUD_ITEM = "Assembly Rooms Pocket Program"; // ---------- ball state ---------- list PROG = []; // [title, music] stride 2 list PART = []; // [avKey, name, role, isOpen] stride 4 list CLIENTS = []; // [objKey, avKey] stride 2 list CLAIMS = []; // [danceNo, leadKey, followKey] stride 3 list PENDING = []; // [reqId, askerKey, toKey, danceNo, time] stride 5 list PRESENTED = []; // [toKey, reqId] stride 2 list CTX = []; // [avKey, ctx, page, targetString] stride 4 integer CURRENT = 0; // 0 = not begun, 1..N = now dancing, N+1 = concluded integer REQUESTS_OPEN = TRUE; integer REQ_SEQ = 0; integer DIALOG_CH; integer DIRTY = FALSE; integer LAST_SAVE = 0; integer TICKS = 0; key PROGRAM_KEY = NULL_KEY; // ---------- notecard loading ---------- integer LOAD_PHASE = 0; // 0 idle, 1 program, 2 save integer LOAD_LINE = 0; key LOAD_Q = NULL_KEY; integer BOOTING = TRUE; // ============================================================ // SMALL HELPERS // ============================================================ tell(key av, string msg) { llRegionSayTo(av, 0, msg); } tellLines(key av, list lines) { string buf = ""; integer i; integer len = llGetListLength(lines); for (i = 0; i < len; i++) { string s = llList2String(lines, i); if (llStringLength(buf) + llStringLength(s) > 900) { tell(av, "\n" + buf); buf = ""; } if (buf != "") buf += "\n"; buf += s; } if (buf != "") tell(av, "\n" + buf); } string clip(string s, integer n) { if (llStringLength(s) > n) return llGetSubString(s, 0, n - 2) + "."; return s; } string swap(string s, string a, string b) { return llDumpList2String(llParseStringKeepNulls(s, [a], []), b); } // strip characters that would break draw strings or our data packets string clean(string s) { s = swap(s, ";", ","); s = swap(s, "|", "/"); s = swap(s, "~", "-"); s = swap(s, "^", "-"); return llStringTrim(s, STRING_TRIM); } integer isMC(key av) { if (av == llGetOwner()) return TRUE; if (llListFindList(MC_NAMES, [llToLower(llKey2Name(av))]) != -1) return TRUE; return FALSE; } // ============================================================ // PROGRAM // ============================================================ integer progCount() { return llGetListLength(PROG) / 2; } string danceTitle(integer n) { return llList2String(PROG, (n - 1) * 2); } string danceMusic(integer n) { return llList2String(PROG, (n - 1) * 2 + 1); } string nowText() { integer total = progCount(); if (total == 0) return "No program has been loaded"; if (CURRENT == 0) return "The ball will begin shortly"; if (CURRENT > total) return "The ball has concluded"; return "Now dancing: No. " + (string)CURRENT + ", " + danceTitle(CURRENT); } // ============================================================ // PARTICIPANTS // ============================================================ integer partIdx(key av) { return llListFindList(PART, [av]); } string partName(key av) { integer i = partIdx(av); if (i == -1) return llKey2Name(av); return llList2String(PART, i + 1); } string partRole(key av) { integer i = partIdx(av); if (i == -1) return "Either"; return llList2String(PART, i + 2); } integer partOpen(key av) { integer i = partIdx(av); if (i == -1) return FALSE; return llList2Integer(PART, i + 3); } list clientsOf(key av) { list result = []; integer i; integer len = llGetListLength(CLIENTS); for (i = 0; i < len; i += 2) { if (llList2Key(CLIENTS, i + 1) == av) result += [llList2Key(CLIENTS, i)]; } return result; } // ============================================================ // CLAIMS (who dances which dance with whom) // ============================================================ key slotPartner(integer n, key av) { integer i; integer len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { if (llList2Integer(CLAIMS, i) == n) { if (llList2Key(CLAIMS, i + 1) == av) return llList2Key(CLAIMS, i + 2); if (llList2Key(CLAIMS, i + 2) == av) return llList2Key(CLAIMS, i + 1); } } return NULL_KEY; } integer leadsIn(integer n, key av) { integer i; integer len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { if (llList2Integer(CLAIMS, i) == n && llList2Key(CLAIMS, i + 1) == av) return TRUE; } return FALSE; } integer bookedCount(key av) { integer c = 0; integer i; integer len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { if (llList2Key(CLAIMS, i + 1) == av || llList2Key(CLAIMS, i + 2) == av) c++; } return c; } integer pairCount(key a, key b) { integer c = 0; integer i; integer len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { key l = llList2Key(CLAIMS, i + 1); key f = llList2Key(CLAIMS, i + 2); if ((l == a && f == b) || (l == b && f == a)) c++; } return c; } integer couplesIn(integer n) { integer c = 0; integer i; integer len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { if (llList2Integer(CLAIMS, i) == n) c++; } return c; } // ---------- roles ---------- integer canLead(string r) { return r != "Follow"; } integer canFollow(string r) { return r != "Lead"; } integer compatible(key a, key b) { string ra = partRole(a); string rb = partRole(b); if (canLead(ra) && canFollow(rb)) return TRUE; if (canFollow(ra) && canLead(rb)) return TRUE; return FALSE; } // TRUE if the asker (a) would lead. "Either" + "Either": the one who asks leads. integer askerLeads(key a, key b) { string ra = partRole(a); string rb = partRole(b); if (ra == "Lead") return TRUE; if (ra == "Follow") return FALSE; if (rb == "Lead") return FALSE; return TRUE; } // ============================================================ // ETIQUETTE // ============================================================ string pairReason(key a, key b) { string bn = partName(b); if (partIdx(a) == -1) return "Kindly collect your Pocket Program (gentlemen) or Dance Card (ladies) from the Assembly Rooms Board before requesting a dance."; if (partIdx(b) == -1) return bn + " is not carrying a dance card this evening."; if (a == b) return "One cannot very well dance with oneself."; if (!REQUESTS_OPEN) return "I beg your pardon, but the Master of Ceremonies has closed dance requests for the moment."; if (!partOpen(b)) return "I beg your pardon, but " + bn + "'s dance card is presently closed."; if (!compatible(a, b)) return "Alas, you and " + bn + " both wish to " + llToLower(partRole(a)) + ". One of you might set your role to Either."; if (pairCount(a, b) >= MAX_PER_PARTNER) return "Pardon me, but propriety suggests you have claimed your share of dances with " + bn + "."; if (MAX_DANCES > 0) { if (bookedCount(a) >= MAX_DANCES) return "Your own dance card is quite full."; if (bookedCount(b) >= MAX_DANCES) return "I regret to inform you that " + bn + "'s dance card is quite full."; } return ""; } list openDances(key a, key b) { list result = []; integer total = progCount(); integer n; for (n = CURRENT + 1; n <= total; n++) { if (slotPartner(n, a) == NULL_KEY && slotPartner(n, b) == NULL_KEY) result += [n]; } return result; } // ============================================================ // REQUESTS // ============================================================ integer pendingIdx(integer reqId) { integer i; integer len = llGetListLength(PENDING); for (i = 0; i < len; i += 5) { if (llList2Integer(PENDING, i) == reqId) return i; } return -1; } integer pendingBetween(key a, key b) { integer i; integer len = llGetListLength(PENDING); for (i = 0; i < len; i += 5) { if (llList2Key(PENDING, i + 1) == a && llList2Key(PENDING, i + 2) == b) return i; } return -1; } beginRequest(key a, key b) { string reason = pairReason(a, b); if (reason != "") { tell(a, reason); return; } if (pendingBetween(a, b) != -1) { tell(a, "Your request to " + partName(b) + " is already under consideration. Kindly await a reply."); return; } if (llGetListLength(openDances(a, b)) == 0) { tell(a, "Regrettably, no dances remain open on both your card and " + partName(b) + "'s."); return; } showDancePicker(a, b, 0); } submitRequest(key a, key b, integer n) { string reason = pairReason(a, b); if (reason != "") { tell(a, reason); return; } if (llListFindList(openDances(a, b), [n]) == -1) { tell(a, "No. " + (string)n + " is no longer open. Please choose another dance."); return; } if (pendingBetween(a, b) != -1) { tell(a, "Your request to " + partName(b) + " is already under consideration."); return; } REQ_SEQ++; PENDING += [REQ_SEQ, a, b, n, llGetUnixTime()]; tell(a, "Your request for No. " + (string)n + ", " + danceTitle(n) + ", has been presented to " + partName(b) + ". Please await a reply."); presentNext(b); } // Show the recipient their OLDEST waiting request - one dialog at a time. presentNext(key to) { if (llListFindList(PRESENTED, [to]) != -1) return; integer i; integer len = llGetListLength(PENDING); for (i = 0; i < len; i += 5) { if (llList2Key(PENDING, i + 2) == to) { integer reqId = llList2Integer(PENDING, i); key asker = llList2Key(PENDING, i + 1); integer n = llList2Integer(PENDING, i + 3); // the answer clock starts when the dialog is shown PENDING = llListReplaceList(PENDING, [llGetUnixTime()], i + 4, i + 4); PRESENTED += [to, reqId]; string who; if (askerLeads(asker, to)) who = partName(asker) + " would lead."; else who = "You would lead."; string t = "A Dance Request\n\n" + partName(asker) + " requests the honour of\nNo. " + (string)n + ", " + danceTitle(n); if (danceMusic(n) != "") t += "\n(" + danceMusic(n) + ")"; t += "\n\n" + who + "\nYour card: " + (string)bookedCount(to) + " of " + (string)progCount() + " dances taken."; t += "\nPlease reply within " + (string)REQUEST_TIMEOUT + " seconds."; // the request number is IN the buttons, so a stale dialog can never // answer a different gentleman's (or lady's) request llDialog(to, clip(t, 500), ["Accept #" + (string)reqId, "Decline #" + (string)reqId], DIALOG_CH); return; } } } string acceptReason(key asker, key to, integer n) { if (partIdx(asker) == -1 || partIdx(to) == -1) return "one of you no longer carries a card."; if (n <= CURRENT) return "that dance has already begun or passed."; if (slotPartner(n, asker) != NULL_KEY || slotPartner(n, to) != NULL_KEY) return "No. " + (string)n + " has since been spoken for."; if (!compatible(asker, to)) return "your roles no longer pair."; if (pairCount(asker, to) >= MAX_PER_PARTNER) return "the limit of dances together has been reached."; if (MAX_DANCES > 0) { if (bookedCount(asker) >= MAX_DANCES || bookedCount(to) >= MAX_DANCES) return "a dance card is full."; } return ""; } answerRequest(key to, integer reqId, integer accepted) { integer p = llListFindList(PRESENTED, [to]); if (p == -1 || llList2Integer(PRESENTED, p + 1) != reqId) { tell(to, "That request has already expired or been answered."); return; } PRESENTED = llDeleteSubList(PRESENTED, p, p + 1); integer i = pendingIdx(reqId); if (i == -1) { tell(to, "That request has expired."); presentNext(to); return; } key asker = llList2Key(PENDING, i + 1); integer n = llList2Integer(PENDING, i + 3); PENDING = llDeleteSubList(PENDING, i, i + 4); string askerName = partName(asker); string toName = partName(to); string dance = "No. " + (string)n + ", " + danceTitle(n); if (accepted) { string reason = acceptReason(asker, to, n); if (reason != "") { tell(to, "Alas, this dance can no longer be arranged: " + reason); tell(asker, toName + " wished to accept, but alas: " + reason); } else { if (askerLeads(asker, to)) CLAIMS += [n, asker, to]; else CLAIMS += [n, to, asker]; DIRTY = TRUE; tell(to, "You have accepted " + dance + " with " + askerName + "."); tell(asker, toName + " graciously accepts your invitation for " + dance + "!"); sendCard(asker); sendCard(to); drawBoard(); } } else { tell(to, "You have politely declined " + askerName + "."); tell(asker, toName + " must regretfully decline your kind invitation for " + dance + " at this time."); } presentNext(to); } expireRequests() { integer now = llGetUnixTime(); integer i; for (i = llGetListLength(PENDING) - 5; i >= 0; i -= 5) { integer reqId = llList2Integer(PENDING, i); key asker = llList2Key(PENDING, i + 1); key to = llList2Key(PENDING, i + 2); integer age = now - llList2Integer(PENDING, i + 4); integer p = llListFindList(PRESENTED, [to]); integer shown = FALSE; if (p != -1) { if (llList2Integer(PRESENTED, p + 1) == reqId) shown = TRUE; } // shown requests expire on the timeout; queued ones get triple the time if ((shown && age > REQUEST_TIMEOUT) || (!shown && age > REQUEST_TIMEOUT * 3)) { PENDING = llDeleteSubList(PENDING, i, i + 4); tell(asker, "Your request to " + partName(to) + " has expired. Do feel free to ask again."); if (shown) { PRESENTED = llDeleteSubList(PRESENTED, p, p + 1); tell(to, "The dance request from " + partName(asker) + " has expired."); presentNext(to); } } } } // drop requests for dances that have begun or no longer exist purgeStaleRequests() { integer total = progCount(); integer i; for (i = llGetListLength(PENDING) - 5; i >= 0; i -= 5) { integer n = llList2Integer(PENDING, i + 3); if (n <= CURRENT || n > total) { integer reqId = llList2Integer(PENDING, i); key asker = llList2Key(PENDING, i + 1); key to = llList2Key(PENDING, i + 2); PENDING = llDeleteSubList(PENDING, i, i + 4); tell(asker, "Your request to " + partName(to) + " for No. " + (string)n + " has lapsed - that dance is under way."); integer p = llListFindList(PRESENTED, [to]); if (p != -1) { if (llList2Integer(PRESENTED, p + 1) == reqId) { PRESENTED = llDeleteSubList(PRESENTED, p, p + 1); presentNext(to); } } } } } releaseDance(key av, integer n) { integer i; integer len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { if (llList2Integer(CLAIMS, i) == n) { key l = llList2Key(CLAIMS, i + 1); key f = llList2Key(CLAIMS, i + 2); if (l == av || f == av) { key other = l; if (l == av) other = f; CLAIMS = llDeleteSubList(CLAIMS, i, i + 2); DIRTY = TRUE; tell(av, "You have released No. " + (string)n + ", " + danceTitle(n) + "."); tell(other, partName(av) + " regrets that they must release No. " + (string)n + ", " + danceTitle(n) + ". That dance is open on your card once more."); sendCard(av); sendCard(other); drawBoard(); return; } } } tell(av, "That dance is not on your card."); } // ============================================================ // DIALOGS // ============================================================ setCtx(key av, string ctx, integer page, key target) { integer i = llListFindList(CTX, [av]); if (i != -1) CTX = llDeleteSubList(CTX, i, i + 3); CTX += [av, ctx, page, (string)target]; // target stored as STRING on purpose } // generic paged picker: up to 9 items per page plus navigation showPicker(key av, string header, list labels, list lines, integer page) { integer total = llGetListLength(labels); integer pages = (total + 8) / 9; if (pages < 1) pages = 1; if (page >= pages) page = pages - 1; if (page < 0) page = 0; integer first = page * 9; integer last = first + 8; if (last >= total) last = total - 1; list buttons = []; if (page > 0) buttons += ["<< Prev"]; else buttons += ["-"]; buttons += ["Cancel"]; if (page < pages - 1) buttons += ["Next >>"]; else buttons += ["-"]; string t = header; if (total > 0) { t += "\n\n" + llDumpList2String(llList2List(lines, first, last), "\n"); buttons += llList2List(labels, first, last); } if (pages > 1) t += "\n\nPage " + (string)(page + 1) + " of " + (string)pages; llDialog(av, clip(t, 500), buttons, DIALOG_CH); } showDancePicker(key a, key b, integer page) { list ns = openDances(a, b); list labels = []; list lines = []; integer i; integer len = llGetListLength(ns); for (i = 0; i < len; i++) { integer n = llList2Integer(ns, i); labels += ["No. " + (string)n]; lines += ["No. " + (string)n + " " + clip(danceTitle(n), 30)]; } string who = partName(b); string lead; if (askerLeads(a, b)) lead = "You would lead."; else lead = who + " would lead."; setCtx(a, "PICKDANCE", page, b); showPicker(a, "Request a Dance with " + who + "\n" + lead + "\nWhich dance would you request?", labels, lines, page); } list presentPartners(key a) { list result = []; integer i; integer len = llGetListLength(PART); for (i = 0; i < len; i += 4) { key k = llList2Key(PART, i); if (k != a && llList2Integer(PART, i + 3) && llGetAgentSize(k) != ZERO_VECTOR) result += [k]; } return result; } showPeople(key a, integer page) { if (partIdx(a) == -1) { tell(a, "Kindly collect your Pocket Program (gentlemen) or Dance Card (ladies) from the Assembly Rooms Board first."); return; } list ks = presentPartners(a); integer len = llGetListLength(ks); if (len == 0) { tell(a, "No other dancers with open cards are present just now."); return; } list labels = []; list lines = []; integer i; for (i = 0; i < len; i++) { key k = llList2Key(ks, i); labels += ["#" + (string)(i + 1)]; lines += ["#" + (string)(i + 1) + " " + clip(partName(k), 26) + " (" + partRole(k) + ")"]; } setCtx(a, "PEOPLE", page, NULL_KEY); showPicker(a, "Ask to Dance\nWhom would you ask?", labels, lines, page); } showRelease(key av, integer page) { list labels = []; list lines = []; integer total = progCount(); integer n; for (n = CURRENT + 1; n <= total; n++) { key p = slotPartner(n, av); if (p != NULL_KEY) { labels += ["Release " + (string)n]; lines += ["No. " + (string)n + " " + clip(danceTitle(n), 22) + " - " + clip(partName(p), 22)]; } } if (llGetListLength(labels) == 0) { tell(av, "You have no upcoming dances to release."); return; } setCtx(av, "RELEASE", page, NULL_KEY); showPicker(av, "Release a Dance\nWhich engagement must you release?\n(Your partner will be told politely.)", labels, lines, page); } showOwnerMenu(key av) { if (partIdx(av) == -1) { tell(av, "Kindly collect your Pocket Program (gentlemen) or Dance Card (ladies) from the Assembly Rooms Board first."); return; } string open = "Open"; string toggle = "Close Card"; if (!partOpen(av)) { open = "Closed"; toggle = "Open Card"; } string t = "Your Dance Card\n" + BALL_NAME + "\n\n" + nowText(); t += "\n\nRole: " + partRole(av) + "\nCard: " + open; t += "\nDances: " + (string)bookedCount(av) + " of " + (string)progCount(); if (MAX_DANCES > 0) t += " (limit " + (string)MAX_DANCES + ")"; setCtx(av, "OWNER", 0, NULL_KEY); llDialog(av, clip(t, 500), ["My Card", "Program", "Done", "Ask to Dance", "Release", "My Role", toggle], DIALOG_CH); } showRoleMenu(key av) { setCtx(av, "ROLE", 0, NULL_KEY); llDialog(av, "Your Role\n\nCurrently: " + partRole(av) + "\n\nLead - you lead every dance\nFollow - you follow every dance\nEither - you will lead or follow", ["Lead", "Follow", "Either", "Back"], DIALOG_CH); } showGuestMenu(key av) { list b = ["Program", "Done"]; if (llGetInventoryType(CARD_ITEM) == INVENTORY_OBJECT) b += ["Lady's Card"]; if (llGetInventoryType(HUD_ITEM) == INVENTORY_OBJECT) b += ["Pocket Program"]; string t = BALL_NAME + "\n\n" + nowText() + "\n\n"; if (partIdx(av) != -1) { b += ["My Card", "Ask to Dance", "Card Menu"]; t += "Welcome back to the ballroom."; } else t += "Welcome! Ladies, take a Dance Card for your wrist. Gentlemen, take a Pocket Program (HUD). Wear it to join the ball."; setCtx(av, "GUEST", 0, NULL_KEY); llDialog(av, clip(t, 500), b, DIALOG_CH); } showMCMenu(key av) { string req = "Open"; string reqBtn = "Close Requests"; if (!REQUESTS_OPEN) { req = "Closed"; reqBtn = "Open Requests"; } string startBtn = "Next Dance"; if (CURRENT == 0) startBtn = "Start Ball"; string t = BALL_NAME + "\nMaster of Ceremonies\n\n" + nowText(); t += "\n\nRequests: " + req; t += "\nDancers: " + (string)(llGetListLength(PART) / 4); t += "\nWaiting requests: " + (string)(llGetListLength(PENDING) / 5); setCtx(av, "MC", 0, NULL_KEY); llDialog(av, clip(t, 500), ["Guest Menu", "Done", "Reset Ball", "Reload Prog", "Dancers", "Announce", startBtn, "Prev Dance", reqBtn], DIALOG_CH); } // ============================================================ // CHAT LISTINGS // ============================================================ showProgram(key av) { list L = [BALL_NAME + " - Programme of Dances"]; integer total = progCount(); integer n; for (n = 1; n <= total; n++) { string s = "No. " + (string)n + " " + danceTitle(n); if (danceMusic(n) != "") s += " (" + danceMusic(n) + ")"; if (n == CURRENT) s += " << NOW"; else if (n < CURRENT) s += " (done)"; L += [s]; } if (total == 0) L += ["No program has been loaded."]; tellLines(av, L); } showMyCard(key av) { if (partIdx(av) == -1) { tell(av, "Kindly collect your Pocket Program (gentlemen) or Dance Card (ladies) from the Assembly Rooms Board first."); return; } list L = ["Dance card of " + partName(av) + " (" + partRole(av) + ") - " + BALL_NAME]; integer total = progCount(); integer n; for (n = 1; n <= total; n++) { key p = slotPartner(n, av); string s = "No. " + (string)n + " " + danceTitle(n) + ": "; if (p == NULL_KEY) s += "open"; else { s += "with " + partName(p); if (leadsIn(n, av)) s += " (you lead)"; else s += " (they lead)"; } if (n == CURRENT) s += " << NOW"; else if (n < CURRENT) s += " (done)"; L += [s]; } tellLines(av, L); } showDancers(key av) { list L = [BALL_NAME + " - Dancers"]; integer i; integer len = llGetListLength(PART); for (i = 0; i < len; i += 4) { key k = llList2Key(PART, i); string s = llList2String(PART, i + 1) + " - " + llList2String(PART, i + 2) + " - " + (string)bookedCount(k) + " dances"; if (!llList2Integer(PART, i + 3)) s += " - card closed"; if (llGetAgentSize(k) == ZERO_VECTOR) s += " - away"; L += [s]; } if (len == 0) L += ["No dancers have registered yet."]; tellLines(av, L); } // ============================================================ // MC ACTIONS // ============================================================ announce() { integer total = progCount(); if (CURRENT == 0) { llSay(0, BALL_NAME + ": the ball will begin shortly. Do fill your dance cards!"); return; } if (CURRENT > total) { llSay(0, "Honoured guests, " + BALL_NAME + " has concluded. Thank you for a splendid evening!"); return; } string t = "Honoured guests, the next dance is No. " + (string)CURRENT + ", " + danceTitle(CURRENT); if (danceMusic(CURRENT) != "") t += " (" + danceMusic(CURRENT) + ")"; llSay(0, t + "."); string line = "Partners, please take the floor: "; integer count = 0; integer i; integer len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { if (llList2Integer(CLAIMS, i) == CURRENT) { key l = llList2Key(CLAIMS, i + 1); key f = llList2Key(CLAIMS, i + 2); string pair = partName(l) + " with " + partName(f); if (llStringLength(line) + llStringLength(pair) > 900) { llSay(0, line); line = ""; } if (count > 0 && line != "") line += "; "; line += pair; count++; tell(l, "Your partner for " + danceTitle(CURRENT) + " is " + partName(f) + ". You lead."); tell(f, "Your partner for " + danceTitle(CURRENT) + " is " + partName(l) + ". " + partName(l) + " leads."); } } if (count == 0) llSay(0, "No partners are engaged on the cards for this dance - all are welcome to the floor!"); else if (line != "") llSay(0, line); } sendAllCards() { integer i; integer len = llGetListLength(PART); for (i = 0; i < len; i += 4) sendCard(llList2Key(PART, i)); } advance(integer delta) { integer total = progCount(); CURRENT += delta; if (CURRENT < 0) CURRENT = 0; if (CURRENT > total + 1) CURRENT = total + 1; DIRTY = TRUE; purgeStaleRequests(); drawBoard(); sendAllCards(); if (delta > 0) announce(); } resetBall() { PART = []; CLIENTS = []; CLAIMS = []; PENDING = []; PRESENTED = []; CTX = []; CURRENT = 0; REQUESTS_OPEN = TRUE; if (llGetInventoryType(SAVE_CARD) == INVENTORY_NOTECARD) llRemoveInventory(SAVE_CARD); DIRTY = FALSE; drawBoard(); llRegionSay(BOARD_CH, "BOARD_UP"); // worn cards re-register fresh } // ============================================================ // DANCE CARD / POCKET PROGRAM COMMUNICATION // ============================================================ registerClient(key obj) { key av = llGetOwnerKey(obj); if (av == obj) return; // object vanished if (llGetAgentSize(av) == ZERO_VECTOR) return; // owner not in region if (partIdx(av) == -1) { string nm = clip(clean(llKey2Name(av)), 30); PART += [av, nm, "Either", TRUE]; DIRTY = TRUE; tell(av, "Welcome to The Assembly Rooms and " + BALL_NAME + ", " + nm + ". You are entered in the register. Touch your Dance Card or Pocket Program for your menu. Gentlemen request a dance by touching a lady's wrist card."); } if (llListFindList(CLIENTS, [obj]) == -1) CLIENTS += [obj, av]; llRegionSayTo(obj, BOARD_CH, "WELCOME"); sendCard(av); } // Packet: H|ball|name|role|open|current|total then E|n~title~partner~pos^... then END sendCard(key av) { list objs = clientsOf(av); integer olen = llGetListLength(objs); if (olen == 0) return; string open = "Open"; if (!partOpen(av)) open = "Closed"; integer total = progCount(); list msgs = ["H|" + clip(BALL_NAME, 40) + "|" + partName(av) + "|" + partRole(av) + "|" + open + "|" + (string)CURRENT + "|" + (string)total]; string chunk = ""; integer n; for (n = 1; n <= total; n++) { key p = slotPartner(n, av); string pn = ""; string pos = ""; if (p != NULL_KEY) { pn = clip(partName(p), 22); if (leadsIn(n, av)) pos = "L"; else pos = "F"; } string e = (string)n + "~" + clip(danceTitle(n), 26) + "~" + pn + "~" + pos; if (llStringLength(chunk) + llStringLength(e) > 800) { msgs += ["E|" + chunk]; chunk = ""; } if (chunk != "") chunk += "^"; chunk += e; } if (chunk != "") msgs += ["E|" + chunk]; msgs += ["END"]; integer mlen = llGetListLength(msgs); integer i; integer j; for (i = 0; i < olen; i++) { key o = llList2Key(objs, i); for (j = 0; j < mlen; j++) llRegionSayTo(o, BOARD_CH, llList2String(msgs, j)); } } pruneClients() { integer i; for (i = llGetListLength(CLIENTS) - 2; i >= 0; i -= 2) { if (llKey2Name(llList2Key(CLIENTS, i)) == "") CLIENTS = llDeleteSubList(CLIENTS, i, i + 1); } } // ============================================================ // BOARD FACE // ============================================================ drawBoard() { integer total = progCount(); string d = ""; d += "PenColour Black; FontSize 44; MoveTo 40,24; Text " + clip(BALL_NAME, 36) + ";"; d += "PenColour DarkRed; FontSize 26; MoveTo 40,92; Text " + nowText() + ";"; string req = "Dance requests are OPEN - wear your card and touch to begin"; if (!REQUESTS_OPEN) req = "Dance requests are CLOSED for the moment"; d += "PenColour Navy; FontSize 20; MoveTo 40,134; Text " + req + ";"; d += "PenColour Black; PenSize 2; MoveTo 30,170; LineTo 994,170;"; integer step = 40; integer fs = 22; if (total > 16) { step = 38; fs = 20; } integer y = 186; integer n; for (n = 1; n <= total; n++) { string colour = "Black"; string mark = ""; if (n < CURRENT) colour = "Gray"; if (n == CURRENT) { colour = "DarkRed"; mark = ">> "; } d += "PenColour " + colour + "; FontSize " + (string)fs + "; MoveTo 40," + (string)y + "; Text " + mark + "No. " + (string)n + " " + clip(danceTitle(n), 28) + ";"; if (danceMusic(n) != "") { d += "FontSize " + (string)(fs - 4) + "; MoveTo 480," + (string)(y + 3) + "; Text " + clip(danceMusic(n), 32) + ";"; } integer c = couplesIn(n); if (c > 0) { string cs = (string)c + " couples"; if (c == 1) cs = "1 couple"; d += "FontSize " + (string)(fs - 4) + "; MoveTo 880," + (string)(y + 3) + "; Text " + cs + ";"; } y += step; } if (total == 0) { d += "PenColour DarkRed; FontSize 24; MoveTo 40,220; Text Add dances to the Ball Program notecard;"; } d += "PenColour Gray; FontSize 18; MoveTo 40,986; Text The Assembly Rooms Dance Card System - Conducted with Propriety - touch for the menu;"; osSetDynamicTextureData("", "vector", d, "1024", 4); } // ============================================================ // PERSISTENCE // ============================================================ saveState() { list L = ["# Ball Save - written automatically by the Assembly Rooms Board. Do not edit.", "S|" + (string)CURRENT + "|" + (string)REQUESTS_OPEN + "|" + (string)REQ_SEQ]; integer i; integer len = llGetListLength(PART); for (i = 0; i < len; i += 4) { L += ["P|" + llList2String(PART, i) + "|" + llList2String(PART, i + 1) + "|" + llList2String(PART, i + 2) + "|" + (string)llList2Integer(PART, i + 3)]; } len = llGetListLength(CLAIMS); for (i = 0; i < len; i += 3) { L += ["C|" + (string)llList2Integer(CLAIMS, i) + "|" + llList2String(CLAIMS, i + 1) + "|" + llList2String(CLAIMS, i + 2)]; } if (llGetInventoryType(SAVE_CARD) == INVENTORY_NOTECARD) { llRemoveInventory(SAVE_CARD); llSleep(0.2); } osMakeNotecard(SAVE_CARD, L); DIRTY = FALSE; LAST_SAVE = llGetUnixTime(); } startLoad(integer phase) { LOAD_PHASE = phase; LOAD_LINE = 0; if (phase == 1) { // defaults, so a removed setting reverts BALL_NAME = "The Grand Ball"; MAX_PER_PARTNER = 2; MAX_DANCES = 0; REQUEST_TIMEOUT = 60; MC_NAMES = []; CARD_ITEM = "Assembly Rooms Dance Card"; HUD_ITEM = "Assembly Rooms Pocket Program"; PROG = []; if (llGetInventoryType(PROGRAM_CARD) != INVENTORY_NOTECARD) { llOwnerSay("No '" + PROGRAM_CARD + "' notecard found. Add one to load your dances."); programLoaded(); return; } PROGRAM_KEY = llGetInventoryKey(PROGRAM_CARD); LOAD_Q = llGetNotecardLine(PROGRAM_CARD, 0); } else { if (llGetInventoryType(SAVE_CARD) != INVENTORY_NOTECARD) { finishLoad(); return; } LOAD_Q = llGetNotecardLine(SAVE_CARD, 0); } } parseProgramLine(string s) { s = llStringTrim(s, STRING_TRIM); if (s == "") return; if (llGetSubString(s, 0, 0) == "#") return; integer eq = llSubStringIndex(s, "="); if (eq > 0) { string k = llToUpper(llStringTrim(llGetSubString(s, 0, eq - 1), STRING_TRIM)); string v = llStringTrim(llGetSubString(s, eq + 1, -1), STRING_TRIM); if (k == "BALL_NAME") BALL_NAME = clean(v); else if (k == "MAX_PER_PARTNER") MAX_PER_PARTNER = (integer)v; else if (k == "MAX_DANCES") MAX_DANCES = (integer)v; else if (k == "REQUEST_TIMEOUT") REQUEST_TIMEOUT = (integer)v; else if (k == "CARD_ITEM") CARD_ITEM = v; else if (k == "HUD_ITEM") HUD_ITEM = v; else if (k == "MC") { list names = llParseString2List(v, [","], []); integer i; integer len = llGetListLength(names); for (i = 0; i < len; i++) { string nm = llToLower(llStringTrim(llList2String(names, i), STRING_TRIM)); if (nm != "") MC_NAMES += [nm]; } } return; } if (progCount() >= MAX_PROGRAM) return; list parts = llParseStringKeepNulls(s, ["|"], []); string title = clean(llList2String(parts, 0)); string music = ""; if (llGetListLength(parts) > 1) music = clean(llList2String(parts, 1)); if (title != "") PROG += [title, music]; } parseSaveLine(string s) { list p = llParseStringKeepNulls(s, ["|"], []); string t = llList2String(p, 0); if (t == "S" && llGetListLength(p) >= 4) { CURRENT = (integer)llList2String(p, 1); REQUESTS_OPEN = (integer)llList2String(p, 2); REQ_SEQ = (integer)llList2String(p, 3); } else if (t == "P" && llGetListLength(p) >= 5) { key av = (key)llList2String(p, 1); if (partIdx(av) == -1) PART += [av, llList2String(p, 2), llList2String(p, 3), (integer)llList2String(p, 4)]; } else if (t == "C" && llGetListLength(p) >= 4) { integer n = (integer)llList2String(p, 1); if (n >= 1 && n <= progCount()) CLAIMS += [n, (key)llList2String(p, 2), (key)llList2String(p, 3)]; } } programLoaded() { if (BOOTING) { startLoad(2); return; } // MC reload mid-ball: keep everything, drop claims beyond the new program LOAD_PHASE = 0; integer total = progCount(); integer i; for (i = llGetListLength(CLAIMS) - 3; i >= 0; i -= 3) { if (llList2Integer(CLAIMS, i) > total) CLAIMS = llDeleteSubList(CLAIMS, i, i + 2); } if (CURRENT > total + 1) CURRENT = total + 1; purgeStaleRequests(); DIRTY = TRUE; drawBoard(); sendAllCards(); llOwnerSay("Ball Program reloaded: " + (string)total + " dances."); } finishLoad() { LOAD_PHASE = 0; BOOTING = FALSE; integer total = progCount(); if (CURRENT > total + 1) CURRENT = total + 1; drawBoard(); llRegionSay(BOARD_CH, "BOARD_UP"); // ask any worn cards to check in llOwnerSay("Assembly Rooms Board ready: " + (string)total + " dances, " + (string)(llGetListLength(PART) / 4) + " dancers on record."); } // ============================================================ // MAIN // ============================================================ default { state_entry() { llSetText("", ZERO_VECTOR, 0.0); // no hover text, ever DIALOG_CH = -100000 - (integer)llFrand(900000000.0); llListen(DIALOG_CH, "", NULL_KEY, ""); llListen(BOARD_CH, "", NULL_KEY, ""); BOOTING = TRUE; startLoad(1); llSetTimerEvent(5.0); } on_rez(integer p) { llResetScript(); } changed(integer c) { if (c & CHANGED_OWNER) llResetScript(); if (c & CHANGED_INVENTORY) { // only a NEW Ball Program triggers a reload (our own saves don't) if (LOAD_PHASE == 0 && llGetInventoryKey(PROGRAM_CARD) != PROGRAM_KEY) { llOwnerSay("Ball Program changed - reloading."); startLoad(1); } } } touch_start(integer total_number) { integer i; for (i = 0; i < total_number; i++) { key av = llDetectedKey(i); if (LOAD_PHASE != 0) tell(av, "The Assembly Rooms Board is preparing. One moment, please."); else if (isMC(av)) showMCMenu(av); else showGuestMenu(av); } } dataserver(key q, string data) { if (q != LOAD_Q) return; if (data == EOF) { if (LOAD_PHASE == 1) programLoaded(); else finishLoad(); return; } if (LOAD_PHASE == 1) parseProgramLine(data); else parseSaveLine(data); LOAD_LINE++; if (LOAD_PHASE == 1) LOAD_Q = llGetNotecardLine(PROGRAM_CARD, LOAD_LINE); else LOAD_Q = llGetNotecardLine(SAVE_CARD, LOAD_LINE); } listen(integer ch, string name, key id, string msg) { // ---------------- dance cards & pocket programs ---------------- if (ch == BOARD_CH) { if (LOAD_PHASE != 0) return; if (msg == "HELLO") registerClient(id); else if (msg == "BYE") { integer ci = llListFindList(CLIENTS, [id]); if (ci != -1) CLIENTS = llDeleteSubList(CLIENTS, ci, ci + 1); } else if (llSubStringIndex(msg, "TOUCH|") == 0) { if (llListFindList(CLIENTS, [id]) == -1) registerClient(id); key owner = llGetOwnerKey(id); key toucher = (key)llGetSubString(msg, 6, -1); if (toucher == owner) showOwnerMenu(owner); else beginRequest(toucher, owner); } return; } // ---------------- dialogs ---------------- if (ch != DIALOG_CH) return; // answers carry their own request number if (llSubStringIndex(msg, "Accept #") == 0) { answerRequest(id, (integer)llGetSubString(msg, 8, -1), TRUE); return; } if (llSubStringIndex(msg, "Decline #") == 0) { answerRequest(id, (integer)llGetSubString(msg, 9, -1), FALSE); return; } integer c = llListFindList(CTX, [id]); if (c == -1) return; string ctx = llList2String(CTX, c + 1); integer page = llList2Integer(CTX, c + 2); key target = (key)llList2String(CTX, c + 3); if (msg == "Done" || msg == "Cancel" || msg == "-") return; // ----- paging ----- if (msg == "<< Prev" || msg == "Next >>") { if (msg == "<< Prev") page--; else page++; if (ctx == "PICKDANCE") showDancePicker(id, target, page); else if (ctx == "PEOPLE") showPeople(id, page); else if (ctx == "RELEASE") showRelease(id, page); return; } // ----- picker selections ----- if (ctx == "PICKDANCE" && llSubStringIndex(msg, "No. ") == 0) { submitRequest(id, target, (integer)llGetSubString(msg, 4, -1)); return; } if (ctx == "PEOPLE" && llSubStringIndex(msg, "#") == 0) { list ks = presentPartners(id); integer k = (integer)llGetSubString(msg, 1, -1) - 1; if (k >= 0 && k < llGetListLength(ks)) beginRequest(id, llList2Key(ks, k)); else tell(id, "That dancer is no longer available."); return; } if (ctx == "RELEASE" && llSubStringIndex(msg, "Release ") == 0) { integer rn = (integer)llGetSubString(msg, 8, -1); if (rn > CURRENT) releaseDance(id, rn); else tell(id, "That dance has already begun."); return; } // ----- role ----- if (ctx == "ROLE") { if (msg == "Lead" || msg == "Follow" || msg == "Either") { integer ri = partIdx(id); if (ri != -1) { PART = llListReplaceList(PART, [msg], ri + 2, ri + 2); DIRTY = TRUE; tell(id, "Your role is now: " + msg + ". Dances already on your card are unchanged."); sendCard(id); } } showOwnerMenu(id); return; } // ----- shared dancer buttons ----- if (msg == "My Card") { showMyCard(id); return; } if (msg == "Program") { showProgram(id); return; } if (msg == "Ask to Dance") { showPeople(id, 0); return; } if (msg == "Release") { showRelease(id, 0); return; } if (msg == "My Role") { showRoleMenu(id); return; } if (msg == "Card Menu") { showOwnerMenu(id); return; } if (msg == "Close Card" || msg == "Open Card") { integer oi = partIdx(id); if (oi != -1) { integer nowOpen = (msg == "Open Card"); PART = llListReplaceList(PART, [nowOpen], oi + 3, oi + 3); DIRTY = TRUE; if (nowOpen) tell(id, "Your dance card is open to requests."); else tell(id, "Your dance card is closed to new requests."); sendCard(id); } showOwnerMenu(id); return; } if (msg == "Lady's Card") { if (llGetInventoryType(CARD_ITEM) == INVENTORY_OBJECT) { llGiveInventory(id, CARD_ITEM); tell(id, "Your Dance Card has been delivered. Wear it on your wrist so gentlemen may request a dance."); } return; } if (msg == "Pocket Program") { if (llGetInventoryType(HUD_ITEM) == INVENTORY_OBJECT) { llGiveInventory(id, HUD_ITEM); tell(id, "Your Pocket Program has been delivered. Wear it as a HUD, then touch a lady's wrist card to request a dance."); } return; } // ----- MC only from here ----- if (!isMC(id)) return; if (msg == "Guest Menu") { showGuestMenu(id); return; } if (msg == "Start Ball" || msg == "Next Dance") { advance(1); showMCMenu(id); return; } if (msg == "Prev Dance") { advance(-1); showMCMenu(id); return; } if (msg == "Announce") { announce(); return; } if (msg == "Dancers") { showDancers(id); return; } if (msg == "Open Requests" || msg == "Close Requests") { REQUESTS_OPEN = (msg == "Open Requests"); DIRTY = TRUE; drawBoard(); showMCMenu(id); return; } if (msg == "Reload Prog") { llOwnerSay("Reloading the Ball Program..."); startLoad(1); return; } if (msg == "Reset Ball") { setCtx(id, "RESET", 0, NULL_KEY); llDialog(id, "Reset the Ball?\n\nThis clears ALL dancers, dance cards and requests, and returns to before the first dance. The program is kept.", ["Yes, Reset", "No"], DIALOG_CH); return; } if (msg == "Yes, Reset" && ctx == "RESET") { resetBall(); tell(id, "The ball has been reset. Worn cards will check in afresh."); return; } if (msg == "No") { showMCMenu(id); return; } } timer() { expireRequests(); TICKS++; if (TICKS >= 12) { TICKS = 0; pruneClients(); } if (DIRTY && LOAD_PHASE == 0 && llGetUnixTime() - LAST_SAVE >= 15) saveState(); } }