Экспорты и state bag'и
Два способа войти, и сначала дешёвый.
phoneapi <id> выводит в консоль сервера всё из того, что описано ниже, для одного игрока. Если что-то здесь отвечает не так, как вы ожидаете, запустите эту команду, прежде чем читать дальше, — это быстрее, чем гадать, на чьей стороне ошибка.
State bag’и — ответы, которые ничего не стоят
Заголовок раздела «State bag’и — ответы, которые ничего не стоят»State bag не требует callback’а, события или договорённости с этим ресурсом. Это чтение таблицы на той машине, которая спрашивает; он переживает перезапуск этого ресурса у вас под ногами и работает одинаково с клиента и с сервера.
-- serverif Player(src).state.onCallWith then ... end
-- client, about somebody elseif Player(GetPlayerFromServerId(id)).state.phoneOpen then ... end| Bag | Тип | |
|---|---|---|
phoneNumber | string | номер телефона, который у них с собой |
phoneName | string | имя, которое этот телефон показывает при звонке другому |
phoneDevice | string | nil | какой это предмет-телефон, в режиме предметов. nil, когда телефоны принадлежат персонажам |
phoneOpen | boolean | телефон вынут и виден на экране |
onCallWith | number | nil | source на другом конце |
callAnswered | boolean | кто-то снял трубку. false, пока ещё звонит |
speakerphone | boolean | |
mutedCall | boolean | они отключили собственный микрофон |
otherMutedCall | boolean | другая сторона отключила свой микрофон |
flashlight | boolean | их фонарик включён. Каждый клиент рисует луч на этом педе |
phoneCamera | boolean | открыт видоискатель: они вот-вот сделают фотографию |
phoneSignal | number | сколько делений, от 0 до Config.Signal.Bars. 0 — телефон, до которого город не может достучаться |
Три вещи, которые стоит знать:
Их записывает сервер. Клиент тоже знает всё это и мог бы опубликовать это меньшим числом строк, но клиент может солгать обо всём этом, а bag, который читает античит, должен стоить того, чтобы его читали.
Звонок идёт — значит, человек в разговоре. onCallWith устанавливается в момент, когда телефон начинает звонить, потому что линия занята, — а вот что пока не верно, так это то, что кто-то ответил, и для этого есть callAnswered.
onCallWith может быть nil во время настоящего звонка. На другом конце может быть чей-то браузер, а не игрок в городе. callAnswered при этом по-прежнему true; просто нет source, который можно было бы назвать.
Bag’а flashlight не существовало, пока фонарик действительно что-то не освещал, исходя из того, что bag, который всегда false, хуже, чем bag, которого нет: первое — отсутствующая функция, второе — неверный ответ. Теперь он что-то освещает, поэтому публикуется.
Серверные экспорты
Заголовок раздела «Серверные экспорты»exports.mic_phone:GetNumber(src) --> '555-010-2048'exports.mic_phone:GetName(src) --> 'Jean Dupont'exports.mic_phone:GetIdentifier(src) --> 'char1:abc…' or 'device:abc…' in item modeexports.mic_phone:GetDevice(src) --> the phone item's id, or nilexports.mic_phone:HasPhone(src) --> are they carrying one at allexports.mic_phone:IsOpen(src)exports.mic_phone:IsInCall(src)exports.mic_phone:CallWith(src) --> the other source, or nil
exports.mic_phone:GetSource(number) --> src, for a numberexports.mic_phone:NumberOf(identifier) --> works for somebody who is not onlineexports.mic_phone:IdentifierOf(number)
exports.mic_phone:Call(src, number) --> rings it from their phone, exactly as dialling wouldexports.mic_phone:EndCall(src)exports.mic_phone:AddContact(src, name, number, { email =, notes =, favorite = })exports.mic_phone:RemoveContact(src, number)exports.mic_phone:SendNotification(src, { app =, title =, body = })exports.mic_phone:OpenApp(src, 'messages')exports.mic_phone:OpenPhone(src) / ClosePhone(src)
exports.mic_phone:SendMessage(number, text, from)exports.mic_phone:SendMail(identifierOrNumber, fromName, fromAddress, subject, body)exports.mic_phone:SendInvoice(senderSrc, targetNumber, label, amount, society)Call возвращает то, что сообщили бы самому телефону, — { ok = false, reason = 'busy' }, 'airplane', 'unknown', 'offline', — потому что он запускает тот же обработчик, что и собственный номеронабиратель телефона. Путь кода один и набор правил один, в которых можно ошибиться.
Экспорт, ответ которого nil, не возвращает значения вообще, даже nil. Если подставить его прямо в список аргументов, он не превращается в "nil" — он сокращает список. Сначала положите его в локальную переменную:
local with = exports.mic_phone:CallWith(src) -- and not print(exports.mic_phone:CallWith(src), x)Сам телефон
Заголовок раздела «Сам телефон»Вопросы, на которые другому ресурсу постоянно приходилось отвечать самостоятельно. Каждый из них — это поиск, который телефон и так выполняет, поэтому путь кода один и набор правил один, в которых можно ошибиться.
exports.mic_phone:GetBattery(src) --> 0-100, or nil when this city has no battery at allexports.mic_phone:SetBattery(src, 40) --> your own charger, or a story that drains oneexports.mic_phone:IsCharging(src)exports.mic_phone:HasAirplaneMode(src) --> the city cannot reach them; their own phone still works
exports.mic_phone:GetSignal(src) --> 0 to Config.Signal.Bars. Also the `phoneSignal` state bagexports.mic_phone:SetJammed(src, true) --> takes their bars away: a heist, a prison, a story
exports.mic_phone:GetGallery(src) --> every picture: { id, url, width, height, kind, created }exports.mic_phone:AddPhoto(src, url, w, h) --> puts one in it, and it appears without a rebootexports.mic_phone:GetContacts(src)
exports.mic_phone:GetInvoices(src) --> every invoice on that phone, paid and unpaidexports.mic_phone:UnpaidInvoices(src) --> count, total
exports.mic_phone:SendLocation(number, x, y, 'The docks', from) --> a map pin in a conversation
exports.mic_phone:GetSocialHandle(src, 'instapic') --> 'nightshift', or nil if they never signed upexports.mic_phone:SocialPost(src, 'twixel', { text = 'Anybody seen a blue Sultan?' })exports.mic_phone:SetVerified(src, 'twixel', true) --> the tick beside their name, per app
exports.mic_phone:GetRadioChannel(src) --> 12, or nilexports.mic_phone:SetRadioChannel(src, 12) --> 0 takes them off itОтвет GetBattery, равный nil, — это не то же самое, что 0: nil означает, что этот сервер никогда не включал батарею. Скрипт, который не различает эти случаи, не даст человеку вызвать скорую в городе, где телефон никогда не разряжается.
AddPhoto проверяет URL по хостам, которые разрешает этот сервер, точно так же, как проверяется фотография, сделанная камерой. Галерея, которая показывает любой адрес, — это галерея, которую кто-то может направить на что угодно.
SocialPost и SetRadioChannel запускают собственные обработчики телефона, поэтому подчиняются тому, чему подчиняется экран: нет аккаунта в этом приложении — нет поста, а канал, который эта работа иметь не может, отклоняется и здесь.
Шесть монет, цены которым задаёт сервер по таймеру, до которого клиент добраться не может. Всё ниже — в монетах и в деньгах, никогда не в миллионных долях, в которых считает ресурс, — поэтому скрипт, выплачивающий кому-то четверть чего-либо, пишет 0.25.
exports.mic_phone:Coins() --> every coin: id, name, symbol, price, change, window, historyexports.mic_phone:CoinPrice('santo') --> 42137.55, in money. nil when there is no such coinexports.mic_phone:GetCrypto(src, 'santo') --> 0.4218exports.mic_phone:GetCryptoValue(src) --> what their whole portfolio is worth, in money
exports.mic_phone:AddCrypto(src, 'omen', 12.5) --> true when it landedexports.mic_phone:RemoveCrypto(src, 'omen', 12.5) --> false when they did not have it, and nothing movesexports.mic_phone:SetCoinPrice('omen', 0.4) --> move the market for a storyRemoveCrypto отказывает, а не уходит в минус, поэтому его ответ — это ещё и «смог ли человек заплатить», — именно то, что на самом деле хочет спросить магазин, продающий что-то за монеты:
if exports.mic_phone:RemoveCrypto(src, 'omen', price) then giveThem(thing) endКошелёк принадлежит персонажу или телефону, и это определяется Config.Apps.Crypto.Wallet. В режиме device кража телефона уносит с собой портфель; в режиме character украденный телефон показывает вору пустое приложение. Экспорты в любом случае принимают source и не обязаны знать, какой это режим.
phonecrypto в консоли выводит рынок, phonecrypto <id> — то, чем владеет этот игрок, а phonecrypto price <coin> <money> двигает цену вручную.
Батарея и фонарик
Заголовок раздела «Батарея и фонарик»Оба включаются и выключаются в конфиге — shared/config/battery.lua и Config.Flashlight, — и по умолчанию батарея выключена, а фонарик включён.
exports.mic_phone:BatteryLevel(src) --> 0-100, or 100 when the battery is switched offexports.mic_phone:IsPhoneCharging(src)exports.mic_phone:ChargePhone(src, true) -- plug in; false unplugsЗарядка не обязана быть предметом в кармане. Config.Battery.Charger.Item даёт вам такую из коробки, а ChargePhone — для всего остального: кабель в машине, розетка на стене, проп за столом.
phonebattery [charge|unplug|full|empty] <id> меняет уровень, и именно так проверяют батарею, не испытывая собственное терпение.
Команды администратора
Заголовок раздела «Команды администратора»У администраторов в игре (и в консоли) есть несколько команд, чтобы выяснить, почему что-то делает не то, что вы задумали. Они только смотрят или намеренно меняют телефон одного игрока; ничего из них не выдумывает.
phoneapi <id> | state bag’и, экспорты и батарея одного игрока |
phoneapps | список приложений в том виде, как его разрешил конфиг |
phonedevice [backup|restore] <id> | какой телефон они держат и какие копии у них хранятся |
phonebattery [charge|unplug|full|empty] <id> |
phoneapi также выводит, есть ли у этого телефона сигнал и что для него удерживается, — это цепочка от «нет сигнала» до «уведомление пришло» в двух строках. Она проходит через четыре места, и раньше не было способа увидеть, в каком из них что-то пошло не так.
Уровень — это не таймер. Строка запоминает, где он был, когда и что делал телефон — открыт, в кармане или заряжается, — а текущий уровень — это арифметика на часах. Сто игроков стоят сто чисел, а не сто таймеров, и телефон, оставленный на зарядке на время перезапуска сервера, возвращается заряженным, а не замороженным.
Как не пустить вашу подсказку в чужой фотоальбом
Заголовок раздела «Как не пустить вашу подсказку в чужой фотоальбом»(Это и остальное про сосуществование — пед, проп, режим предметов — собрано в разделе ваш ресурс рядом с телефоном.)
Фотография — это весь кадр. Всё, что рисовал ваш ресурс в момент нажатия затвора, — подсказка, подпись маркера, индикатор выполнения — попадает на снимок, и этот ресурс не может запретить вам рисовать.
phoneCamera существует, чтобы вы могли остановить себя сами. Он равен true с того момента, как открывается видоискатель, и до его закрытия, а одна строка в цикле отрисовки — это и есть всё исправление:
if LocalPlayer.state.phoneCamera then goto skip endСобственный HUD телефона уже убран с дороги: Config.Apps.Camera.HideHud скрывает миникарту и полоски здоровья в игре, пока камера открыта, так что человек получает именно то, что кадрирует.
Фонарик — это state bag flashlight, поэтому каждый другой клиент рисует луч на педе этого игрока. Он светит, только пока телефон вынут, и гаснет, когда телефон убирают.
Клиентские экспорты
Заголовок раздела «Клиентские экспорты»exports.mic_phone:Open() / Close() / Toggle() / IsOpen()exports.mic_phone:CanOpen() --> ok, reasonexports.mic_phone:Restricted() --> 'Dead' | 'Cuffed' | 'Swimming' | … | nilexports.mic_phone:OpenApp('bank') / CloseApp()exports.mic_phone:GetNumber() / GetDevice() / HasPhone() / IsBooted()exports.mic_phone:IsInCall() / IsTablet() / SetTablet(true)exports.mic_phone:Notify({ app =, title =, body = })Ничто здесь не обходит проверки, которые телефон применяет к себе сам. Open и OpenApp получают тот же отказ, который получил бы игрок, нажавший F1, по тем же причинам: телефон, который открывается, пока его владелец мёртв, потому что ресурс вежливо попросил, — это не телефон с правилами. CanOpen и Restricted существуют, чтобы вы могли спросить до того, как просить, и затемнить кнопку, а не предлагать такую, которая ничего не делает.
Названия
Заголовок раздела «Названия»Названия bag’ов совпадают с названиями lb-phone там, где смысл тот же, так что скрипт, написанный под тот телефон, читает этот без изменений.